|
|
|
|
@ -535,23 +535,23 @@ export function MapView({
|
|
|
|
|
|
|
|
|
|
if (filtered.length === 0) return null;
|
|
|
|
|
|
|
|
|
|
// 경위도 바운드 계산
|
|
|
|
|
let minLon = Infinity,
|
|
|
|
|
maxLon = -Infinity,
|
|
|
|
|
minLat = Infinity,
|
|
|
|
|
maxLat = -Infinity;
|
|
|
|
|
// 경위도 바운드 계산 (raw 값 별도 보존 — 캡 마스크 좌표 계산에 사용)
|
|
|
|
|
let rawMinLon = Infinity,
|
|
|
|
|
rawMaxLon = -Infinity,
|
|
|
|
|
rawMinLat = Infinity,
|
|
|
|
|
rawMaxLat = -Infinity;
|
|
|
|
|
for (const p of dispersionHeatmap) {
|
|
|
|
|
if (p.lon < minLon) minLon = p.lon;
|
|
|
|
|
if (p.lon > maxLon) maxLon = p.lon;
|
|
|
|
|
if (p.lat < minLat) minLat = p.lat;
|
|
|
|
|
if (p.lat > maxLat) maxLat = p.lat;
|
|
|
|
|
if (p.lon < rawMinLon) rawMinLon = p.lon;
|
|
|
|
|
if (p.lon > rawMaxLon) rawMaxLon = p.lon;
|
|
|
|
|
if (p.lat < rawMinLat) rawMinLat = p.lat;
|
|
|
|
|
if (p.lat > rawMaxLat) rawMaxLat = p.lat;
|
|
|
|
|
}
|
|
|
|
|
const padLon = (maxLon - minLon) * 0.02;
|
|
|
|
|
const padLat = (maxLat - minLat) * 0.02;
|
|
|
|
|
minLon -= padLon;
|
|
|
|
|
maxLon += padLon;
|
|
|
|
|
minLat -= padLat;
|
|
|
|
|
maxLat += padLat;
|
|
|
|
|
const padLon = (rawMaxLon - rawMinLon) * 0.02;
|
|
|
|
|
const padLat = (rawMaxLat - rawMinLat) * 0.02;
|
|
|
|
|
const minLon = rawMinLon - padLon;
|
|
|
|
|
const maxLon = rawMaxLon + padLon;
|
|
|
|
|
const minLat = rawMinLat - padLat;
|
|
|
|
|
const maxLat = rawMaxLat + padLat;
|
|
|
|
|
|
|
|
|
|
// 캔버스에 농도 이미지 렌더링
|
|
|
|
|
const W = 1200,
|
|
|
|
|
@ -596,6 +596,38 @@ export function MapView({
|
|
|
|
|
ctx.fill();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 우측 끝 반타원 캡 마스크 — 격자 경계에서 직선으로 잘리는 현상 처리
|
|
|
|
|
const tipX = ((rawMaxLon - minLon) / (maxLon - minLon)) * W;
|
|
|
|
|
const lonExtent = rawMaxLon - rawMinLon;
|
|
|
|
|
const rightThreshLon = rawMaxLon - lonExtent * 0.1;
|
|
|
|
|
const rightPts = filtered.filter((p) => p.lon >= rightThreshLon);
|
|
|
|
|
let capCenterY = H / 2;
|
|
|
|
|
let capHalfH = H / 3;
|
|
|
|
|
if (rightPts.length > 0) {
|
|
|
|
|
const ys = rightPts.map((p) => (1 - (p.lat - minLat) / (maxLat - minLat)) * H);
|
|
|
|
|
const minY = Math.min(...ys);
|
|
|
|
|
const maxY = Math.max(...ys);
|
|
|
|
|
capCenterY = (minY + maxY) / 2;
|
|
|
|
|
capHalfH = (maxY - minY) / 2 + 15;
|
|
|
|
|
}
|
|
|
|
|
// capStartX: 타원 중심 (tipX에서 타원 x반경만큼 왼쪽)
|
|
|
|
|
// 마스크 = 사각형(0..capStartX) + 반타원(capStartX..tipX)
|
|
|
|
|
// → tipX 오른쪽으로 타원이 넘어가지 않아 실제 클리핑이 발생함
|
|
|
|
|
const capRadiusX = capHalfH * 0.7;
|
|
|
|
|
const capStartX = Math.max(0, tipX - capRadiusX);
|
|
|
|
|
const maskCanvas = document.createElement('canvas');
|
|
|
|
|
maskCanvas.width = W;
|
|
|
|
|
maskCanvas.height = H;
|
|
|
|
|
const mctx = maskCanvas.getContext('2d')!;
|
|
|
|
|
mctx.fillStyle = 'black';
|
|
|
|
|
mctx.fillRect(0, 0, capStartX, H);
|
|
|
|
|
mctx.beginPath();
|
|
|
|
|
mctx.ellipse(capStartX, capCenterY, capRadiusX, capHalfH, 0, -Math.PI / 2, Math.PI / 2);
|
|
|
|
|
mctx.fill();
|
|
|
|
|
ctx.globalCompositeOperation = 'destination-in';
|
|
|
|
|
ctx.drawImage(maskCanvas, 0, 0);
|
|
|
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
|
|
|
|
|
|
|
|
// Canvas → data URL 변환 (interleaved MapboxOverlay에서 HTMLCanvasElement 직접 전달 시 렌더링 안 되는 문제 회피)
|
|
|
|
|
const imageUrl = canvas.toDataURL('image/png');
|
|
|
|
|
return {
|
|
|
|
|
|