refactor: Phase 4-1 — geometry 유틸 추출 (FleetClusterLayer 979줄→927줄)
- utils/geometry.ts: convexHull, padPolygon, clusterColor 추출 (48줄) - FleetClusterLayer: 로컬 함수 → import로 교체
This commit is contained in:
부모
728936439b
커밋
c6c3b5ffb9
@ -5,6 +5,7 @@ import type { MapLayerMouseEvent } from 'maplibre-gl';
|
||||
import type { Ship, VesselAnalysisDto } from '../../types';
|
||||
import { fetchFleetCompanies } from '../../services/vesselAnalysis';
|
||||
import type { FleetCompany } from '../../services/vesselAnalysis';
|
||||
import { convexHull, padPolygon, clusterColor } from '../../utils/geometry';
|
||||
|
||||
export interface SelectedGearGroupData {
|
||||
parent: Ship | null;
|
||||
@ -28,59 +29,6 @@ interface Props {
|
||||
onSelectedFleetChange?: (data: SelectedFleetData | null) => void;
|
||||
}
|
||||
|
||||
// 두 벡터의 외적 (2D) — Graham scan에서 왼쪽 회전 여부 판별
|
||||
function cross(o: [number, number], a: [number, number], b: [number, number]): number {
|
||||
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
||||
}
|
||||
|
||||
// Graham scan 기반 볼록 껍질 (반시계 방향)
|
||||
function convexHull(points: [number, number][]): [number, number][] {
|
||||
const n = points.length;
|
||||
if (n < 2) return points.slice();
|
||||
const sorted = points.slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
||||
const lower: [number, number][] = [];
|
||||
for (const p of sorted) {
|
||||
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) {
|
||||
lower.pop();
|
||||
}
|
||||
lower.push(p);
|
||||
}
|
||||
const upper: [number, number][] = [];
|
||||
for (let i = sorted.length - 1; i >= 0; i--) {
|
||||
const p = sorted[i];
|
||||
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) {
|
||||
upper.pop();
|
||||
}
|
||||
upper.push(p);
|
||||
}
|
||||
// lower + upper (첫/끝 중복 제거)
|
||||
lower.pop();
|
||||
upper.pop();
|
||||
return lower.concat(upper);
|
||||
}
|
||||
|
||||
// 중심에서 각 꼭짓점 방향으로 padding 확장
|
||||
function padPolygon(hull: [number, number][], padding: number): [number, number][] {
|
||||
if (hull.length === 0) return hull;
|
||||
const cx = hull.reduce((s, p) => s + p[0], 0) / hull.length;
|
||||
const cy = hull.reduce((s, p) => s + p[1], 0) / hull.length;
|
||||
return hull.map(([x, y]) => {
|
||||
const dx = x - cx;
|
||||
const dy = y - cy;
|
||||
const len = Math.sqrt(dx * dx + dy * dy);
|
||||
if (len === 0) return [x + padding, y + padding] as [number, number];
|
||||
const scale = (len + padding) / len;
|
||||
return [cx + dx * scale, cy + dy * scale] as [number, number];
|
||||
});
|
||||
}
|
||||
|
||||
// cluster_id 해시 → HSL 색상
|
||||
function clusterColor(id: number): string {
|
||||
const h = (id * 137) % 360;
|
||||
return `hsl(${h}, 80%, 55%)`;
|
||||
}
|
||||
|
||||
// HSL 문자열 → hex 근사 (MapLibre는 hsl() 지원하므로 직접 사용 가능)
|
||||
// GeoJSON feature에 color 속성으로 주입
|
||||
interface ClusterPolygonFeature {
|
||||
type: 'Feature';
|
||||
|
||||
50
frontend/src/utils/geometry.ts
Normal file
50
frontend/src/utils/geometry.ts
Normal file
@ -0,0 +1,50 @@
|
||||
/** 두 벡터의 외적 (2D) — Graham scan에서 왼쪽 회전 여부 판별 */
|
||||
function cross(o: [number, number], a: [number, number], b: [number, number]): number {
|
||||
return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
||||
}
|
||||
|
||||
/** Graham scan 기반 볼록 껍질 (반시계 방향) */
|
||||
export function convexHull(points: [number, number][]): [number, number][] {
|
||||
const n = points.length;
|
||||
if (n < 2) return points.slice();
|
||||
const sorted = points.slice().sort((a, b) => a[0] - b[0] || a[1] - b[1]);
|
||||
const lower: [number, number][] = [];
|
||||
for (const p of sorted) {
|
||||
while (lower.length >= 2 && cross(lower[lower.length - 2], lower[lower.length - 1], p) <= 0) {
|
||||
lower.pop();
|
||||
}
|
||||
lower.push(p);
|
||||
}
|
||||
const upper: [number, number][] = [];
|
||||
for (let i = sorted.length - 1; i >= 0; i--) {
|
||||
const p = sorted[i];
|
||||
while (upper.length >= 2 && cross(upper[upper.length - 2], upper[upper.length - 1], p) <= 0) {
|
||||
upper.pop();
|
||||
}
|
||||
upper.push(p);
|
||||
}
|
||||
lower.pop();
|
||||
upper.pop();
|
||||
return lower.concat(upper);
|
||||
}
|
||||
|
||||
/** 중심에서 각 꼭짓점 방향으로 padding 확장 */
|
||||
export function padPolygon(hull: [number, number][], padding: number): [number, number][] {
|
||||
if (hull.length === 0) return hull;
|
||||
const cx = hull.reduce((s, p) => s + p[0], 0) / hull.length;
|
||||
const cy = hull.reduce((s, p) => s + p[1], 0) / hull.length;
|
||||
return hull.map(([x, y]) => {
|
||||
const dx = x - cx;
|
||||
const dy = y - cy;
|
||||
const len = Math.sqrt(dx * dx + dy * dy);
|
||||
if (len === 0) return [x + padding, y + padding] as [number, number];
|
||||
const scale = (len + padding) / len;
|
||||
return [cx + dx * scale, cy + dy * scale] as [number, number];
|
||||
});
|
||||
}
|
||||
|
||||
/** cluster_id 해시 → HSL 색상 */
|
||||
export function clusterColor(id: number): string {
|
||||
const h = (id * 137) % 360;
|
||||
return `hsl(${h}, 80%, 55%)`;
|
||||
}
|
||||
불러오는 중...
Reference in New Issue
Block a user