OpenStreetMap 데이터와 소상공인 POI 데이터를 활용하여 테마별 산책 경로를 탐색하는 시스템입니다.
사용자가 출발점과 도착점을 선택하면, 두 점을 초점으로 하는 타원 영역 내의 POI(카페, 맛집 등)를 클러스터링하여 표시합니다. 사용자는 원하는 클러스터를 경유지로 선택하고, 최단거리 경로를 생성할 수 있습니다.
- 타원 영역 탐색: 시작점↔도착점을 초점으로, 이동 시간에 따른 타원 범위 설정
- 테마별 POI 클러스터링: 카페, 맛집, 유흥, 쇼핑, 녹지 5개 테마
- 줌 레벨 기반 표시: 줌아웃 시 큰 클러스터, 줌인 시 세밀한 클러스터
- 경유지 선택 및 경로 생성: 클러스터 클릭으로 경유지 추가, 최단경로 자동 계산
OSMProject/
├── src/
│ ├── main_pipeline.py # 전체 파이프라인 실행
│ ├── step1_osm_loader.py # OSM 데이터 로드
│ ├── step2_poi_classifier.py # POI 테마 분류
│ ├── step3_feature_enricher.py # 도로에 POI 정보 연결
│ ├── step4_linegraph_builder.py # 라인 그래프 변환
│ ├── step5_route_visualizer.py # 기본 경로 시각화
│ ├── step5_route_visualizer_v2.py # K-diverse 경로 시각화
│ ├── step6_ellipse_cluster_explorer.py # ⭐ 타원 클러스터 탐색기 생성
│ ├── ellipse_cluster_explorer.html # 생성된 HTML 파일
│ ├── algorithms/
│ │ ├── k_diverse_paths.py # K개 다양한 경로 알고리즘
│ │ └── time_based_walk.py # 시간 기반 산책 알고리즘
│ └── data/
│ ├── hongdae-poi-classified.csv # 분류된 POI 데이터
│ ├── hongdae-enriched.pkl # 도로 그래프 (POI 연결됨)
│ ├── hongdae-linegraph.pkl # 라인 그래프
│ └── 소상공인시장진흥공단_*.csv # 원본 POI 데이터
├── requirements.txt
└── README.md
# 가상환경 생성 및 활성화
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 의존성 설치
pip install -r requirements.txtcd src
python main_pipeline.pypython src/step6_ellipse_cluster_explorer.pysrc/ellipse_cluster_explorer.html을 브라우저에서 열어 사용합니다.
파일: src/step6_ellipse_cluster_explorer.py
# pre_cluster_pois() 함수 내부 (약 110~115줄)
# ============================================================
# 클러스터링 해상도 설정 (수정 가능)
# ============================================================
CELL_SIZE_SMALL = 150 # 높은 줌 레벨용 (미터)
CELL_SIZE_MEDIUM = 300 # 중간 줌 레벨용 (미터)
CELL_SIZE_LARGE = 500 # 낮은 줌 레벨용 (미터)- 값이 클수록: 클러스터 수 감소, 개별 클러스터 크기 증가
- 값이 작을수록: 클러스터 수 증가, 더 세밀한 표시
파일: src/step6_ellipse_cluster_explorer.py (HTML 템플릿 내 JavaScript)
// updateVisualizationByZoom() 함수
// ============================================================
// 줌 레벨별 클러스터 크기 매핑 (수정 가능)
// ============================================================
const ZOOM_THRESHOLD_LARGE = 15; // 이하: large 클러스터
const ZOOM_THRESHOLD_MEDIUM = 16; // 이하: medium 클러스터
// 그 이상: small 클러스터파일: src/step6_ellipse_cluster_explorer.py (HTML 템플릿 내 JavaScript)
// calculateEllipseParams() 함수
// ============================================================
// 도로 네트워크 우회 비율 (수정 가능)
// ============================================================
// 실제 도로 거리 / 직선 거리 비율
// 값이 클수록 타원이 작아짐 (보수적)
// 값이 작을수록 타원이 커짐 (여유로움)
detourRatio = Math.max(1.0, Math.min(detourRatio, 2.0));파일: src/step2_poi_classifier.py
# THEME_MAPPING 딕셔너리 (약 30~60줄)
THEME_MAPPING = {
'cafe': {
'keywords': ['커피', '카페', 'coffee', ...],
'major_codes': [],
'middle_names': ['커피', '카페', ...]
},
'food': {
'keywords': ['한식', '양식', '치킨', ...],
'major_codes': ['I2'], # 음식업 대분류 코드
...
},
# ... 다른 테마들
}파일: src/step6_ellipse_cluster_explorer.py (HTML 템플릿 내 JavaScript)
// dijkstra() 함수
// 현재: 단순 거리 기반 최단경로
// 수정 가능: 테마 점수 가중치 적용
// 예시: 테마 가중치 적용
const weight = w.length * (1 - 0.5 * themeScore); // 테마 점수 높으면 가중치 감소파일: src/step6_ellipse_cluster_explorer.py (HTML 템플릿 내 JavaScript)
// findRoute() 함수 내 waypointNodes 정렬
// 현재: 시작점에서의 직선 거리순 정렬
// 수정 가능: TSP(외판원 문제) 알고리즘으로 최적 순서 계산
waypointNodes.sort((a, b) => a.distFromStart - b.distFromStart);┌─────────────────┐ ┌──────────────────┐
│ OSM 데이터 │────▶│ step1_osm_loader │
│ (seoul-*.pkl) │ └────────┬─────────┘
└─────────────────┘ │
▼
┌─────────────────┐ ┌────────────────────┐
│ 소상공인 CSV │────▶│ step2_poi_classifier│
└─────────────────┘ └────────┬───────────┘
│
▼
┌────────────────────┐
│ step3_feature_ │
│ enricher │
└────────┬───────────┘
│
▼
┌────────────────────┐
│ step4_linegraph_ │
│ builder │
└────────┬───────────┘
│
▼
┌───────────────────────────────────────────────────┐
│ step6_ellipse_cluster_explorer │
│ ┌─────────────────────────────────────────────┐ │
│ │ 1. 데이터 로드 (PKL, CSV) │ │
│ │ 2. 테마별 사전 클러스터링 (Grid-based) │ │
│ │ 3. HTML 템플릿 생성 (JavaScript 포함) │ │
│ └─────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────┘
│
▼
┌───────────────────────────┐
│ ellipse_cluster_explorer │
│ .html │
└───────────────────────────┘
- 복잡도: O(n) - DBSCAN의 O(n²)보다 훨씬 빠름
- 원리: 위도/경도를 고정 크기 셀로 나누고, 각 셀을 하나의 클러스터로 처리
def grid_cluster(pois, cell_size_meters=100):
# 위도 1도 ≈ 111.32km
lat_cell = cell_size_meters / 111320
# 경도는 위도에 따라 다름
lng_cell = cell_size_meters / (111320 * cos(center_lat))
# O(n): 각 POI를 셀에 할당
for poi in pois:
cell = (int(poi.lat / lat_cell), int(poi.lng / lng_cell))
grid[cell].append(poi)- 타원의 정의: 두 초점(시작점, 도착점)까지의 거리 합이 일정한 점들의 집합
- 2a: 총 이동 가능 거리 (걷는 시간 × 75m/분)
- 우회 비율 보정: 실제 도로 거리 / 직선 거리로 타원 크기 조정
// 타원 내부 판정
function isPointInEllipse(lat, lng, start, end, a) {
const d1 = distance(lat, lng, start.lat, start.lng);
const d2 = distance(lat, lng, end.lat, end.lng);
return (d1 + d2) <= (a * 2); // 거리 합 ≤ 2a
}- 라인 그래프 기반 경로 탐색
- 현재 거리만 고려, 테마 가중치 미적용 (수정 가능)
MIT License
- Fork 후 feature 브랜치 생성
- 알고리즘 수정 시
개발자 가이드섹션의 해당 부분 참고 - 테스트 후 Pull Request 생성