|
|
@@ -60,7 +60,6 @@ export default {
|
|
|
|
|
|
|
|
|
routeGroups: {},
|
|
|
- polylines: [],
|
|
|
privateStyle: {
|
|
|
legend: {}
|
|
|
},
|
|
|
@@ -463,11 +462,24 @@ export default {
|
|
|
basePath = this.buildFallbackLinePath(line.start, line.end, 30);
|
|
|
}
|
|
|
|
|
|
+ // --- 核心优化:物理分散偏移 (Dispersal Offset) ---
|
|
|
+ // 根据 lineIdx 为每条路径应用微小偏移,避免多条线路完全重合
|
|
|
+ // 0.00015 度约等于 15 米,(lineIdx - 3) 将多条线在中心点两侧排开
|
|
|
+ const offsetVal = (Number(lineIdx) - 3) * 0.00015;
|
|
|
+ const applyOffset = (p) => {
|
|
|
+ const lng = p.lng || (p.getLng ? p.getLng() : (Array.isArray(p) ? Number(p[0]) : 0));
|
|
|
+ const lat = p.lat || (p.getLat ? p.getLat() : (Array.isArray(p) ? Number(p[1]) : 0));
|
|
|
+ return [lng + offsetVal, lat + offsetVal];
|
|
|
+ };
|
|
|
+
|
|
|
const segments = this.extractMainStraightSegments(basePath);
|
|
|
const overlays = [];
|
|
|
|
|
|
- segments.forEach((segmentPath, segmentIdx) => {
|
|
|
- if (!Array.isArray(segmentPath) || segmentPath.length < 2) return;
|
|
|
+ segments.forEach((rawSegmentPath, segmentIdx) => {
|
|
|
+ if (!Array.isArray(rawSegmentPath) || rawSegmentPath.length < 2) return;
|
|
|
+
|
|
|
+ // 应用偏移到当前段的所有点
|
|
|
+ const segmentPath = rawSegmentPath.map(p => applyOffset(p));
|
|
|
|
|
|
const polyline = new this.AMap.Polyline({
|
|
|
path: segmentPath,
|
|
|
@@ -509,15 +521,15 @@ export default {
|
|
|
if (p1 && p2) {
|
|
|
// 在 p1 和 p2 之间线性插值
|
|
|
const ratio = (currentTargetDist - pathDistances[foundIdx]) / (pathDistances[foundIdx + 1] - pathDistances[foundIdx]);
|
|
|
- const lng1 = p1.lng || (Array.isArray(p1) ? Number(p1[0]) : 0);
|
|
|
- const lat1 = p1.lat || (Array.isArray(p1) ? Number(p1[1]) : 0);
|
|
|
- const lng2 = p2.lng || (Array.isArray(p2) ? Number(p2[0]) : 0);
|
|
|
- const lat2 = p2.lat || (Array.isArray(p2) ? Number(p2[1]) : 0);
|
|
|
+ const lng1 = Number(p1[0]);
|
|
|
+ const lat1 = Number(p1[1]);
|
|
|
+ const lng2 = Number(p2[0]);
|
|
|
+ const lat2 = Number(p2[1]);
|
|
|
|
|
|
const midLng = lng1 + (lng2 - lng1) * ratio;
|
|
|
const midLat = lat1 + (lat2 - lat1) * ratio;
|
|
|
|
|
|
- const bearing = this.calcBearingDeg({ lng: lng1, lat: lat1 }, { lng: lng2, lat: lat2 });
|
|
|
+ const bearing = this.calcBearingDeg(p1, p2);
|
|
|
const rotation = bearing - 90;
|
|
|
|
|
|
const directionMarker = new this.AMap.Marker({
|
|
|
@@ -543,8 +555,8 @@ export default {
|
|
|
for (let i = 0; i < indices.length; i++) {
|
|
|
const idx = indices[i];
|
|
|
const p = segmentPath[idx];
|
|
|
- const lng = p && typeof p.lng === 'number' ? p.lng : (Array.isArray(p) ? Number(p[0]) : NaN);
|
|
|
- const lat = p && typeof p.lat === 'number' ? p.lat : (Array.isArray(p) ? Number(p[1]) : NaN);
|
|
|
+ const lng = Number(p[0]);
|
|
|
+ const lat = Number(p[1]);
|
|
|
if (Number.isNaN(lng) || Number.isNaN(lat)) continue;
|
|
|
|
|
|
// 确定圆点类型:第一个为start,最后一个为end,其余为normal
|
|
|
@@ -570,18 +582,11 @@ export default {
|
|
|
pickEvenlySpacedIndices(totalPoints, count) {
|
|
|
const total = Math.max(Number(totalPoints) || 0, 0);
|
|
|
const target = Math.max(Number(count) || 0, 0);
|
|
|
- if (total <= 0) return [];
|
|
|
- if (target <= 0) return [];
|
|
|
+ if (total <= 0 || target <= 0) return [];
|
|
|
if (target >= total) return Array.from({ length: total }, (_, i) => i);
|
|
|
if (target === 1) return [0];
|
|
|
|
|
|
- const set = new Set();
|
|
|
- const last = total - 1;
|
|
|
- for (let k = 0; k < target; k += 1) {
|
|
|
- const idx = Math.round((k * last) / (target - 1));
|
|
|
- set.add(idx);
|
|
|
- }
|
|
|
- return Array.from(set).sort((a, b) => a - b);
|
|
|
+ return Array.from({ length: target }, (_, k) => Math.round((k * (total - 1)) / (target - 1)));
|
|
|
},
|
|
|
|
|
|
buildFallbackLinePath(start, end, pointCount) {
|
|
|
@@ -604,7 +609,14 @@ export default {
|
|
|
},
|
|
|
|
|
|
extractMainStraightSegments(path) {
|
|
|
- const points = (path || []).filter(p => p && typeof p.lng === 'number' && typeof p.lat === 'number');
|
|
|
+ const getCoord = (p) => {
|
|
|
+ if (!p) return { lng: NaN, lat: NaN };
|
|
|
+ if (Array.isArray(p)) return { lng: Number(p[0]), lat: Number(p[1]) };
|
|
|
+ if (p.getLng) return { lng: p.getLng(), lat: p.getLat() };
|
|
|
+ return { lng: Number(p.lng), lat: Number(p.lat) };
|
|
|
+ };
|
|
|
+
|
|
|
+ const points = (path || []).map(p => getCoord(p)).filter(p => !isNaN(p.lng) && !isNaN(p.lat));
|
|
|
if (points.length < 2) return [];
|
|
|
|
|
|
const thresholdDeg = 18;
|
|
|
@@ -658,17 +670,24 @@ export default {
|
|
|
return finalSegments.length > 0 ? finalSegments : [points];
|
|
|
},
|
|
|
|
|
|
+ // 内部通用工具:从不同格式的点中提取经纬度数组 [lng, lat]
|
|
|
+ _getCoords(p) {
|
|
|
+ if (!p) return [0, 0];
|
|
|
+ const lng = p.lng || (p.getLng ? p.getLng() : (Array.isArray(p) ? Number(p[0]) : 0));
|
|
|
+ const lat = p.lat || (p.getLat ? p.getLat() : (Array.isArray(p) ? Number(p[1]) : 0));
|
|
|
+ return [lng, lat];
|
|
|
+ },
|
|
|
+
|
|
|
calcBearingDeg(a, b) {
|
|
|
- const latRad = ((a.lat + b.lat) / 2) * Math.PI / 180;
|
|
|
- const dx = (b.lng - a.lng) * Math.cos(latRad);
|
|
|
- const dy = (b.lat - a.lat);
|
|
|
+ const [alng, alat] = this._getCoords(a);
|
|
|
+ const [blng, blat] = this._getCoords(b);
|
|
|
+
|
|
|
+ const latRad = ((alat + blat) / 2) * Math.PI / 180;
|
|
|
+ const dx = (blng - alng) * Math.cos(latRad);
|
|
|
+ const dy = (blat - alat);
|
|
|
|
|
|
- // 使用地理方位角公式:0度为北,顺时针增加
|
|
|
- // atan2(dy, dx) 返回数学角度(0为东,逆时针)
|
|
|
- // geographic_bearing = (90 - math_angle + 360) % 360
|
|
|
const mathAngle = Math.atan2(dy, dx) * 180 / Math.PI;
|
|
|
- let bearing = (90 - mathAngle + 360) % 360;
|
|
|
- return bearing;
|
|
|
+ return (90 - mathAngle + 360) % 360;
|
|
|
},
|
|
|
|
|
|
calcAngleDiffDeg(a, b) {
|
|
|
@@ -678,9 +697,12 @@ export default {
|
|
|
},
|
|
|
|
|
|
calcApproxDistance(a, b) {
|
|
|
- const latRad = ((a.lat + b.lat) / 2) * Math.PI / 180;
|
|
|
- const dx = (b.lng - a.lng) * Math.cos(latRad);
|
|
|
- const dy = (b.lat - a.lat);
|
|
|
+ const [alng, alat] = this._getCoords(a);
|
|
|
+ const [blng, blat] = this._getCoords(b);
|
|
|
+
|
|
|
+ const latRad = ((alat + blat) / 2) * Math.PI / 180;
|
|
|
+ const dx = (blng - alng) * Math.cos(latRad);
|
|
|
+ const dy = (blat - alat);
|
|
|
return Math.sqrt(dx * dx + dy * dy);
|
|
|
},
|
|
|
|
|
|
@@ -688,8 +710,8 @@ export default {
|
|
|
if (!position || !config) return null;
|
|
|
|
|
|
try {
|
|
|
- const lng = Number(position[0] || position.lng);
|
|
|
- const lat = Number(position[1] || position.lat);
|
|
|
+ const lng = position.getLng ? position.getLng() : Number(position[0] !== undefined ? position[0] : position.lng);
|
|
|
+ const lat = position.getLat ? position.getLat() : Number(position[1] !== undefined ? position[1] : position.lat);
|
|
|
if (isNaN(lng) || isNaN(lat)) return null;
|
|
|
|
|
|
// 状态文字:起、终、或者状态配置的首字母
|
|
|
@@ -699,99 +721,58 @@ export default {
|
|
|
|
|
|
const isAbnormal = ["离线", "降级", "故障"].includes(config.name);
|
|
|
const isRoute = ["干线协调", "勤务路线"].includes(config.name);
|
|
|
+ const isStartEnd = type === 'start' || type === 'end';
|
|
|
+
|
|
|
+ // 核心配置映射:减少嵌套逻辑
|
|
|
+ const markerStyle = isStartEnd ? {
|
|
|
+ size: '24px',
|
|
|
+ height: '30px',
|
|
|
+ offset: [-12, -30],
|
|
|
+ zIndex: 120,
|
|
|
+ border: '2px solid #fff'
|
|
|
+ } : (isAbnormal ? {
|
|
|
+ size: '30px',
|
|
|
+ height: '30px',
|
|
|
+ offset: [-15, -15],
|
|
|
+ zIndex: 110,
|
|
|
+ border: 'none'
|
|
|
+ } : {
|
|
|
+ size: '14px',
|
|
|
+ height: '14px',
|
|
|
+ offset: [-9, -9], // 14px + 2px padding * 2 = 18px total
|
|
|
+ zIndex: 100,
|
|
|
+ border: isRoute ? 'none' : '1.5px solid rgba(255,255,255,0.7)'
|
|
|
+ });
|
|
|
|
|
|
- // 动态计算尺寸:起终点最大,异常点次之,普通点最小
|
|
|
- const isStartEnd = type === 'start' || type === 'end';
|
|
|
- const size = isStartEnd ? '24px' : (isAbnormal ? '30px' : '14px');
|
|
|
- const sizeNumber = parseInt(size, 10);
|
|
|
- const paddingNumber = (isAbnormal || isStartEnd) ? 0 : 2;
|
|
|
- const outerSizeNumber = sizeNumber + paddingNumber * 2;
|
|
|
-
|
|
|
- // 边框样式:起终点用实白边框
|
|
|
- const borderStyle = isStartEnd
|
|
|
- ? '2px solid #fff'
|
|
|
- : '1.5px solid rgba(255,255,255,0.7)';
|
|
|
-
|
|
|
- // 生成标记内容
|
|
|
- let markerContent = '';
|
|
|
- if (isStartEnd) {
|
|
|
- const iconPath = type === 'start'
|
|
|
- ? require('@/assets/map/start.png')
|
|
|
- : require('@/assets/map/end.png');
|
|
|
- markerContent = `
|
|
|
- <div class="pure-light-node start-end-node"
|
|
|
- style="
|
|
|
- width: ${size};
|
|
|
- height: 30px;
|
|
|
- background: transparent;
|
|
|
- border: none;
|
|
|
- display: flex;
|
|
|
- justify-content: center;
|
|
|
- align-items: flex-end;
|
|
|
- cursor: pointer;
|
|
|
- transform-origin: bottom center;
|
|
|
- ">
|
|
|
- <img src="${iconPath}" style="width: 100%; height: auto; object-fit: contain; pointer-events: none;" />
|
|
|
- </div>
|
|
|
- `;
|
|
|
- } else if (isAbnormal) {
|
|
|
- const iconName = config.name === '离线' ? 'lixian' : config.name === '降级' ? 'jiangji' : 'guzhang';
|
|
|
- markerContent = `
|
|
|
- <div class="pure-light-node ${isAbnormal ? 'breathe' : ''}"
|
|
|
- style="
|
|
|
- width: ${size};
|
|
|
- height: ${size};
|
|
|
- background: transparent;
|
|
|
- box-shadow: none;
|
|
|
- border: none;
|
|
|
- box-sizing: content-box;
|
|
|
- display: flex;
|
|
|
- justify-content: center;
|
|
|
- align-items: center;
|
|
|
- cursor: pointer;
|
|
|
- padding: ${paddingNumber}px;
|
|
|
- ">
|
|
|
- <img src="${require(`@/assets/images/icon_${iconName}.png`)}" style="width: 100%; height: 100%; object-fit: contain;" />
|
|
|
- </div>
|
|
|
- `;
|
|
|
- } else {
|
|
|
- markerContent = `
|
|
|
- <div class="pure-light-node ${isAbnormal ? 'breathe' : ''} ${isRoute ? 'route-node' : ''}"
|
|
|
- style="
|
|
|
- width: ${size};
|
|
|
- height: ${size};
|
|
|
- background: ${config.color || '#999'};
|
|
|
- box-shadow: ${isRoute ? 'none' : `0 0 8px ${config.color}`};
|
|
|
- border: ${isRoute ? 'none' : borderStyle};
|
|
|
- box-sizing: content-box;
|
|
|
- display: flex;
|
|
|
- justify-content: center;
|
|
|
- align-items: center;
|
|
|
- color: #fff;
|
|
|
- border-radius: 50%;
|
|
|
- cursor: pointer;
|
|
|
- padding: ${paddingNumber}px;
|
|
|
- ">
|
|
|
- <span style="transform: scale(0.8); font-weight: bold; font-size: 12px;">${displayText}</span>
|
|
|
- </div>
|
|
|
- `;
|
|
|
- }
|
|
|
-
|
|
|
- // 计算偏移量:起点终点底部对齐,其他中心对齐
|
|
|
- let markerOffset;
|
|
|
- if (isStartEnd) {
|
|
|
- // 设置 24px 宽 30px 高,偏移量设置为底部中心对齐
|
|
|
- markerOffset = new this.AMap.Pixel(-12, -30);
|
|
|
- } else {
|
|
|
- markerOffset = new this.AMap.Pixel(-Math.round(outerSizeNumber / 2), -Math.round(outerSizeNumber / 2));
|
|
|
- }
|
|
|
+ // 生成标记内容
|
|
|
+ let markerContent = '';
|
|
|
+ if (isStartEnd) {
|
|
|
+ markerContent = `
|
|
|
+ <div class="pure-light-node start-end-node" style="width: ${markerStyle.size}; height: ${markerStyle.height}; background: transparent; border: none; display: flex; justify-content: center; align-items: flex-end; cursor: pointer; transform-origin: bottom center;">
|
|
|
+ <img src="${require(`@/assets/map/${type}.png`)}" style="width: 100%; height: auto; object-fit: contain; pointer-events: none;" />
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ } else if (isAbnormal) {
|
|
|
+ const iconName = config.name === '离线' ? 'lixian' : config.name === '降级' ? 'jiangji' : 'guzhang';
|
|
|
+ markerContent = `
|
|
|
+ <div class="pure-light-node breathe" style="width: ${markerStyle.size}; height: ${markerStyle.height}; background: transparent; border: none; box-sizing: content-box; display: flex; justify-content: center; align-items: center; cursor: pointer; padding: 0;">
|
|
|
+ <img src="${require(`@/assets/images/icon_${iconName}.png`)}" style="width: 100%; height: 100%; object-fit: contain;" />
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ } else {
|
|
|
+ markerContent = `
|
|
|
+ <div class="pure-light-node ${isRoute ? 'route-node' : ''}" style="width: ${markerStyle.size}; height: ${markerStyle.height}; background: ${config.color || '#999'}; box-shadow: ${isRoute ? 'none' : `0 0 8px ${config.color}`}; border: ${markerStyle.border}; box-sizing: content-box; display: flex; justify-content: center; align-items: center; color: #fff; border-radius: 50%; cursor: pointer; padding: 2px;">
|
|
|
+ <span style="transform: scale(0.8); font-weight: bold; font-size: 12px;">${displayText}</span>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+ }
|
|
|
|
|
|
- const marker = new this.AMap.Marker({
|
|
|
- position: [lng, lat],
|
|
|
- zIndex: isStartEnd ? 120 : (isAbnormal ? 110 : 100),
|
|
|
- content: markerContent,
|
|
|
- offset: markerOffset,
|
|
|
- extData: {
|
|
|
+ const marker = new this.AMap.Marker({
|
|
|
+ position: [lng, lat],
|
|
|
+ zIndex: markerStyle.zIndex,
|
|
|
+ content: markerContent,
|
|
|
+ offset: new this.AMap.Pixel(...markerStyle.offset),
|
|
|
+ extData: {
|
|
|
...config,
|
|
|
position: [lng, lat],
|
|
|
statusColor: config.color || '#999',
|