import type { ReactNode } from 'react'; import { EnvironmentOutlined, ExportOutlined } from '@ant-design/icons'; import { Button, Space, Tag, Typography } from 'antd'; import './EmbeddedMapUI.css'; const { Paragraph, Text, Title } = Typography; export type EmbeddedMapUIProps = { latitude: number; longitude: number; zoom?: number; height?: number; title?: string; address?: string; description?: string; markerLabel?: string; className?: string; radiusMeters?: number; lockViewport?: boolean; secondaryMarker?: { latitude: number; longitude: number; label?: string; } | null; overlay?: ReactNode; }; type Bounds = { minLatitude: number; maxLatitude: number; minLongitude: number; maxLongitude: number; }; function clampZoom(zoom: number) { return Math.min(18, Math.max(8, zoom)); } function metersToLatitudeDegrees(meters: number) { return meters / 111320; } function metersToLongitudeDegrees(meters: number, latitude: number) { const safeLatitude = Math.max(-89.9999, Math.min(89.9999, latitude)); const denominator = 111320 * Math.cos((safeLatitude * Math.PI) / 180); return meters / Math.max(denominator, 0.000001); } function createViewUrl(latitude: number, longitude: number, zoom: number) { const params = new URLSearchParams({ mlon: String(longitude), mlat: String(latitude), }); return `https://www.openstreetmap.org/?${params.toString()}#map=${clampZoom(zoom)}/${latitude}/${longitude}`; } function createBounds( latitude: number, longitude: number, radiusMeters: number | undefined, secondaryMarker: EmbeddedMapUIProps['secondaryMarker'], ): Bounds { const coverageMeters = Math.max(radiusMeters ?? 0, secondaryMarker ? 180 : 120, 120) * 1.8; const latitudePadding = metersToLatitudeDegrees(coverageMeters); const longitudePadding = metersToLongitudeDegrees(coverageMeters, latitude); let minLatitude = latitude - latitudePadding; let maxLatitude = latitude + latitudePadding; let minLongitude = longitude - longitudePadding; let maxLongitude = longitude + longitudePadding; if (secondaryMarker) { const extraLatitudePadding = metersToLatitudeDegrees(coverageMeters * 0.35); const extraLongitudePadding = metersToLongitudeDegrees( coverageMeters * 0.35, (latitude + secondaryMarker.latitude) / 2, ); minLatitude = Math.min(minLatitude, secondaryMarker.latitude - extraLatitudePadding); maxLatitude = Math.max(maxLatitude, secondaryMarker.latitude + extraLatitudePadding); minLongitude = Math.min(minLongitude, secondaryMarker.longitude - extraLongitudePadding); maxLongitude = Math.max(maxLongitude, secondaryMarker.longitude + extraLongitudePadding); } return { minLatitude, maxLatitude, minLongitude, maxLongitude, }; } function createEmbedUrl(bounds: Bounds, latitude: number, longitude: number) { const params = new URLSearchParams({ bbox: [ bounds.minLongitude, bounds.minLatitude, bounds.maxLongitude, bounds.maxLatitude, ].join(','), layer: 'mapnik', marker: `${latitude},${longitude}`, }); return `https://www.openstreetmap.org/export/embed.html?${params.toString()}`; } function projectPoint(latitude: number, longitude: number, bounds: Bounds) { const xRatio = (longitude - bounds.minLongitude) / Math.max(bounds.maxLongitude - bounds.minLongitude, 0.000001); const yRatio = (bounds.maxLatitude - latitude) / Math.max(bounds.maxLatitude - bounds.minLatitude, 0.000001); return { x: Math.min(1, Math.max(0, xRatio)) * 100, y: Math.min(1, Math.max(0, yRatio)) * 100, }; } function calculateCircleSizePercent(radiusMeters: number, bounds: Bounds, latitude: number) { const diameterLongitudeDegrees = metersToLongitudeDegrees(radiusMeters * 2, latitude); return (diameterLongitudeDegrees / Math.max(bounds.maxLongitude - bounds.minLongitude, 0.000001)) * 100; } export function EmbeddedMapUI({ latitude, longitude, zoom = 15, height = 360, title = '현장 위치', address, description = '모바일과 데스크톱에서 바로 확인할 수 있는 내장 지도입니다.', markerLabel = '현장', className, radiusMeters, lockViewport = false, secondaryMarker, overlay, }: EmbeddedMapUIProps) { const resolvedZoom = clampZoom(zoom); const viewUrl = createViewUrl(latitude, longitude, resolvedZoom); const bounds = createBounds(latitude, longitude, radiusMeters, secondaryMarker); const embedUrl = createEmbedUrl(bounds, latitude, longitude); const primaryPoint = projectPoint(latitude, longitude, bounds); const secondaryPoint = secondaryMarker ? projectPoint(secondaryMarker.latitude, secondaryMarker.longitude, bounds) : null; const radiusPercent = radiusMeters && radiusMeters > 0 ? calculateCircleSizePercent(radiusMeters, bounds, latitude) : null; return (
}> {markerLabel} {`${latitude.toFixed(5)}, ${longitude.toFixed(5)}`} {title} {description} {address ? {address} : null}