地图组件修改-根据经纬度计算距离

This commit is contained in:
wangxk 2025-04-21 17:43:31 +08:00
parent ba94e48c35
commit 86999cc440
3 changed files with 61031 additions and 89 deletions

File diff suppressed because it is too large Load Diff

View File

@ -10,10 +10,9 @@
import { ref, onMounted, watch, defineEmits, onBeforeUnmount } from 'vue'
import * as d3 from 'd3'
import chinaData from './newJson.json'
// ==================== ====================
const DEFAULT_MAP_SIZE = { width: 800, height: 600 }
const PROJECTION_CENTER = [104, 37] //
const PROJECTION_CENTER = [104, 37]
const SCALE_FACTORS = { normal: 600, fullscreen: 1000 }
// ==================== Props/Emits ====================
@ -21,8 +20,8 @@ const props = defineProps({
coordinates: {
type: Array,
default: () => [
[116.405285, 39.904989], //
[121.472644, 31.231706] //
[116.405285, 39.904989],
[121.472644, 31.231706]
],
validator: value => value.every(coord =>
Array.isArray(coord) && coord.length === 2 &&
@ -40,7 +39,7 @@ const emit = defineEmits(['qvehuan1'])
// ==================== ====================
const mapContainer = ref(null)
const isFullscreen = ref(false)
const isRouteInitialized = ref(false) // initqie
const isRouteInitialized = ref(false)
// ==================== D3 ====================
let svgInstance = null
@ -49,53 +48,57 @@ let markersGroup = null
let zoomBehavior = null
// ==================== ====================
/**
* 创建地理投影配置
* @returns {d3.GeoProjection} D3地理投影实例
*/
const createProjection = () => {
const { width, height } = getContainerSize()
return d3.geoMercator()
const projection = d3.geoMercator()
if (props.coordinates?.length >= 2) {
const validCoords = props.coordinates.filter(c => isValidCoordinate(c))
if (validCoords.length >= 2) {
const routeFeature = {
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: validCoords
}
}
projection.fitExtent(
[[50, 50], [width - 50, height - 50]], //
routeFeature
)
return projection
}
}
return projection
.center(PROJECTION_CENTER)
.scale(isFullscreen.value ? SCALE_FACTORS.fullscreen : SCALE_FACTORS.normal)
.translate([width / 2, height / 2])
}
/**
* 初始化地图基础结构
*/
const initializeMap = () => {
//
if (svgInstance) svgInstance.remove()
const { width, height } = getContainerSize()
const projection = createProjection()
const pathGenerator = d3.geoPath().projection(projection)
// SVG
svgInstance = d3.select(mapContainer.value)
.append('svg')
.attr('width', width)
.attr('height', height)
.style('background-color', '#f0f8ff')
//
const mainGroup = svgInstance.append('g')
mapGroup = mainGroup.append('g').attr('class', 'map-layer')
markersGroup = mainGroup.append('g').attr('class', 'markers-layer')
//
renderChinaMap(pathGenerator)
//
initializeZoomBehavior(mainGroup)
}
/**
* 渲染中国地图路径和文字
* @param {d3.GeoPath} path - 路径生成器
*/
const renderChinaMap = (path) => {
//
mapGroup.append('g')
.selectAll('path')
.data(chinaData.features)
@ -104,8 +107,8 @@ const renderChinaMap = (path) => {
.attr('d', path)
.attr('fill', '#e7e7e7')
.attr('stroke', '#fff')
.attr('stroke-width', 0.3)
//
mapGroup.append('g')
.selectAll('text')
.data(chinaData.features)
@ -114,36 +117,20 @@ const renderChinaMap = (path) => {
.attr('transform', d => `translate(${path.centroid(d)})`)
.text(d => d.properties.name || '')
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.style('font', '12px Arial')
.attr('fill', '#333')
.style('pointer-events', 'none')
}
/**
* 初始化缩放行为
* @param {d3.Selection} group - 需要应用缩放的分组
*/
const initializeZoomBehavior = (group) => {
zoomBehavior = d3.zoom()
.scaleExtent([0.2, 68])
.on('zoom', ({ transform }) => {
group.attr('transform', transform)
})
svgInstance.call(zoomBehavior)
}
// ==================== 线 ====================
/**
* 更新路线和标记点
*/
const updateRoute = () => {
if (!props.coordinates?.length) return
if (!mapGroup) initializeMap()
initializeMap()
const projection = createProjection()
const pathGenerator = d3.geoPath().projection(projection)
//
if (isRouteInitialized.value) {
mapGroup.selectAll('.route-path').remove()
markersGroup.selectAll('*').remove()
@ -151,15 +138,10 @@ const updateRoute = () => {
emit('qvehuan1', isRouteInitialized.value)
}
// 线
renderRoutePath(pathGenerator)
renderMarkers(projection)
}
/**
* 绘制路径动画
* @param {d3.GeoPath} path - 路径生成器
*/
const renderRoutePath = (path) => {
const routeData = {
type: "LineString",
@ -171,35 +153,29 @@ const renderRoutePath = (path) => {
.attr('class', 'route-path')
.attr('d', path)
.attr('fill', 'none')
.attr('stroke', '#f00')
.attr('stroke-width', 0.1)
.attr('stroke-dasharray', function() {
return this.getTotalLength()
})
.attr('stroke-dashoffset', function() {
return this.getTotalLength()
})
.transition()
.duration(2000)
.attr('stroke-dashoffset', 0)
.attr('stroke', '#ff4757')
.attr('stroke-width', 2)
.attr('stroke-linecap', 'round')
}
/**
* 绘制位置标记点
* @param {d3.GeoProjection} projection - 地理投影实例
*/
const renderMarkers = (projection) => {
props.coordinates.forEach(coord => {
props.coordinates.forEach((coord, index) => {
if (!isValidCoordinate(coord)) return
const [x, y] = projection(coord)
//
markersGroup.append('circle')
.attr('cx', x)
.attr('cy', y)
.attr('r', 0.3)
.attr('r', 4) //
.attr('fill', '#ff4757')
.attr('stroke', 'white')
.attr('stroke-width', 0)
.attr('stroke-width', 1)
.raise() //
})
}
@ -213,9 +189,17 @@ const isValidCoordinate = (coord) =>
Array.isArray(coord) && coord.length === 2 &&
!isNaN(coord[0]) && !isNaN(coord[1])
// ==================== ====================
// ==================== ====================
const initializeZoomBehavior = (group) => {
zoomBehavior = d3.zoom()
.scaleExtent([0.2, 68])
.on('zoom', ({ transform }) => {
group.attr('transform', transform)
})
svgInstance.call(zoomBehavior)
}
const handleWindowResize = () => {
if (!isFullscreen.value) return
initializeMap()
updateRoute()
}
@ -225,25 +209,22 @@ const toggleFullscreen = () => {
setTimeout(() => {
initializeMap()
updateRoute()
}, 100) // DOM
}, 100)
}
// ==================== ====================
onMounted(() => {
window.addEventListener('resize', handleWindowResize)
//
initializeMap() // [!code ++]
// coordinates线
if (props.coordinates?.length) { // [!code ++]
updateRoute() // [!code ++]
} // [!code ++]
initializeMap()
if (props.coordinates?.length) {
updateRoute()
}
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleWindowResize)
})
// ==================== ====================
watch(() => props.qvehuan, (newVal) => {
isRouteInitialized.value = newVal
updateRoute()
@ -264,6 +245,8 @@ watch(() => props.coordinates, () => {
transition: all 0.3s ease;
width: 100%;
height: 100%;
border: 1px solid #ddd;
border-radius: 8px;
}
.fullscreen {
@ -274,23 +257,26 @@ watch(() => props.coordinates, () => {
bottom: 0;
margin: 0 !important;
z-index: 1000;
border-radius: 0;
}
.fullscreen-btn {
position: absolute;
top: 10px;
right: 25px;
top: 20px;
right: 30px;
z-index: 1001;
padding: 8px 16px;
background: rgba(0, 0, 0, 0.7);
padding: 10px 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
border: none;
border-radius: 4px;
border-radius: 6px;
cursor: pointer;
transition: opacity 0.2s;
transition: all 0.2s;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}
.fullscreen-btn:hover {
opacity: 0.9;
background: rgba(0, 0, 0, 0.9);
transform: translateY(-1px);
}
</style>

View File

@ -1462,10 +1462,11 @@ function frequency(row: any) {
const eventSource = ref(null)
let index = 0
const dynamicCoordinates = ref([])
let SSEclose
function closeSSE() {
dynamicCoordinates.value.length = 0
// SSEclose :any
let SSEclose = new EventSource(userStore.webApiBaseUrl + '/sse/disconnect/' + userStore.userId)
SSEclose = new EventSource(userStore.webApiBaseUrl + '/sse/disconnect/' + userStore.userId)
eventSource.value?.close()
}
function getSSELink() {
@ -1482,10 +1483,9 @@ function getSSELink() {
console.log('SSE消息:', data)
if (data) {
dynamicCoordinates.value.push([data.lon, data.lat])
if (dynamicCoordinates.value.length > 2) {
dynamicCoordinates.value.shift()
}
// if (dynamicCoordinates.value.length > 2) {
// dynamicCoordinates.value.shift()
// }
lineData.value.push({ x: data.UtcTime, y: data.alt })
}
} catch (err) {