From 9a11ceff9418bf6aa3bde976e7f58db2ec9fe59e Mon Sep 17 00:00:00 2001 From: laoyang Date: Thu, 24 Sep 2026 15:48:18 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B7=AF=E7=BA=BF=E5=9C=B0=E5=9B=BE=EF=BC=9A?= =?UTF-8?q?=E9=87=8D=E5=90=88=E7=9A=84=E6=A0=87=E8=AE=B0=E4=B8=8E=E7=BA=BF?= =?UTF-8?q?=E6=AE=B5=E5=90=84=E8=87=AA=E8=AE=A9=E5=BC=80=EF=BC=88=E6=89=B9?= =?UTF-8?q?=2014=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题:往返行程(A→B 去程 + B→A 返程)两段的**中点完全相同**、线段也 完全重合,后画的那一枚会把先画的整枚盖住,看起来只剩一段。 两类重合分别处理: - **线段重合**:同一段(不分方向,A→B 与 B→A 视为同一键)第 2 次起 沿**法线**错开(长段按比例、短段给下限),像双向车道各占一边。 - **标记重合**:不再只比坐标字符串 —— 坐标不同但屏幕上挨着的两点 (相差几公里的两个地点,在"看全国"的缩放级别下像素位置几乎一致) 同样是重合。改为先问地图「这个经纬度现在在屏幕哪个像素上」 (map.lngLatToContainer),再用贪心排布把撞上的标记按像素左右挪开。 - 到达地红点:同一点位只画一个(往返两端本来就是同一个)。 - **缩放后重排**(zoomend):缩放会改变标记间的屏幕距离,缩小时原本 分开的标记可能又叠回去。重排时**不再 setFitView**(fromZoom 标志), 否则与缩放事件互相触发、还会把用户手动缩放好的视野拉回去。 验证:_page11.php 夹具新增返程节点(哈尔滨 → 青岛),_map12.js 新增 「两两求矩形交集」的几何断言 —— 它先抓出真重合(34×39px 重叠), 修复后 0 重叠并确认往返两枚并排 46px、相邻两枚错开 52px。 本地全链 12 步全绿(渲染检查 354 项、真 AMap 浏览器 13 项); 远端 e2e 353 项 0 失败;部署静态断言、双 PHP 渲染、内容断言全过。 --- templates/activities/show.php | 143 +++++++++++++++++++++++++++++----- 1 file changed, 123 insertions(+), 20 deletions(-) diff --git a/templates/activities/show.php b/templates/activities/show.php index 34f0c9c..37bcf74 100644 --- a/templates/activities/show.php +++ b/templates/activities/show.php @@ -169,55 +169,155 @@ require dirname(__DIR__) . '/comments/_block.php'; map = new AMap.Map(el, { viewMode: '2D', zoom: 5 }); var overlays = []; // 当前画上去的所有覆盖物;地理编码补点后整体重画 + /* ── 防重合(批 14)──────────────────────────────────────── + 两类重合,都要治: + ① 往返:A→B 与 B→A 中点是同一个、线段也完全重合 + → 线段沿**法线**错开(像双向车道各占一边),标记在屏幕上错开; + ② 距离很近的地点(相差几公里):坐标不同,但在"看全国"的缩放级别下 + 像素位置几乎一致 → 只能按**屏幕像素**判定碰撞再错开。 + 所以标记一律按像素排布(与缩放级别同步重算),不再只比坐标字符串。 + 注意:错开只改「怎么画」,不改数据 —— prevEnd 仍用原始坐标。 */ + var MARK_W = 46; // 标记直径 + 间隙(CSS 里圆标 40px) + var MARK_H = 44; + + function segKey(p1, p2) { + var a = p1[0].toFixed(5) + ',' + p1[1].toFixed(5); + var b = p2[0].toFixed(5) + ',' + p2[1].toFixed(5); + return a < b ? a + '|' + b : b + '|' + a; // A→B 与 B→A 同一键 + } + function posKey(p) { + return p[0].toFixed(5) + ',' + p[1].toFixed(5); + } + function markerPos(n) { + var s = n.start, en = n.end; + if (s && en) return [(s.lng + en.lng) / 2, (s.lat + en.lat) / 2]; + if (s) return [s.lng, s.lat]; + if (en) return [en.lng, en.lat]; + return null; + } + /** 同一段重复出现时,沿法线左右交替错开(+1 / -1 / +2 / -2 …) */ + function makeSegOffsetter() { + var seen = {}; + return function (p1, p2) { + var key = segKey(p1, p2); + var n = seen[key] || 0; + seen[key] = n + 1; + if (n === 0) return [p1, p2]; + var dx = p2[0] - p1[0], dy = p2[1] - p1[1]; + var len = Math.sqrt(dx * dx + dy * dy) || 1e-9; + var mag = Math.max(len * 0.06, 0.0008); // 长段按比例、短段给下限 + var k = (n % 2 === 1) ? (n + 1) / 2 : -(n / 2); + var ox = (-dy / len) * mag * k; + var oy = (dx / len) * mag * k; + return [[p1[0] + ox, p1[1] + oy], [p2[0] + ox, p2[1] + oy]]; + }; + } + /** 贪心排布:和已放好的标记撞上就左右交替挪开,返回像素偏移 */ + function makePlacer() { + var placed = []; // 已占用的屏幕坐标(容器像素) + return { + place: function (base, id) { + var dx = 0, guard = 0, k = 0; + while (guard++ < 30) { + var hit = false; + for (var j = 0; j < placed.length; j++) { + var q = placed[j]; + if (Math.abs(base.x + dx - q.x) < MARK_W + && Math.abs(base.y - q.y) < MARK_H) { + hit = true; + break; + } + } + if (!hit) break; + k++; + dx = (k % 2 === 1) ? Math.ceil(k / 2) * MARK_W : -Math.ceil(k / 2) * MARK_W; + } + placed.push({ x: base.x + dx, y: base.y, id: id }); + return dx; + }, + reset: function () { placed = []; }, + }; + } + /* 全量重画:先清掉旧覆盖物再按当前 route 数据画。 - 地理编码是异步的,一个点查回来就重画一次,数据是幂等的。 */ - function drawRoute() { + 地理编码是异步的,一个点查回来就重画一次,数据是幂等的。 + fromZoom=true 表示这次是「用户缩放了」触发的重排:只重排标记, + 不再 setFitView —— 否则会和缩放事件互相触发,还会把用户手动 + 缩放好的视野又拉回去。 */ + function drawRoute(fromZoom) { overlays.forEach(function (o) { map.remove(o); }); overlays = []; var inPath = []; // 节点内:出发地 → 到达地(实线) var hopPath = []; // 节点之间:上一站到达地 → 下一站出发地(虚线) var prevEnd = null; + var shiftSeg = makeSegOffsetter(); + var placer = makePlacer(); // 标记按屏幕像素排布,撞上就挪开 + var dotSeen = {}; // 到达地红点:同一点位只画一个 route.forEach(function (n, i) { var s = n.start, en = n.end; - if (s && en) inPath.push([s.lng, s.lat], [en.lng, en.lat]); - if (s && prevEnd) hopPath.push([prevEnd.lng, prevEnd.lat], [s.lng, s.lat]); + if (s && en) { + var seg = shiftSeg([s.lng, s.lat], [en.lng, en.lat]); + inPath.push(seg[0], seg[1]); + } + if (s && prevEnd) { + var hop = shiftSeg([prevEnd.lng, prevEnd.lat], [s.lng, s.lat]); + hopPath.push(hop[0], hop[1]); + } /* 圆形标记放这段行程线的中点(起终点都有才取中点),缺一头就放有的一头。 ⚠️ position 必须是 [lng, lat] 数组 —— 把 {lng,lat} 对象直接传给 AMap 会抛 Invalid Object,forEach 当场中断、整张地图一个覆盖物 都不剩(批 11 上线后踩的坑,光秃秃地图就是它)。 */ - var pos = (s && en) - ? [(s.lng + en.lng) / 2, (s.lat + en.lat) / 2] - : (s ? [s.lng, s.lat] : (en ? [en.lng, en.lat] : null)); + var pos = markerPos(n); if (pos) { - var html = '
' + /* 先问地图「这个经纬度现在在屏幕哪个像素上」,再和已放的标记比距离: + 坐标不同但屏幕位置挨着(几公里的两个点、或往返的中点)都要挪开 */ + var base = null; + try { + base = map.lngLatToContainer(new AMap.LngLat(pos[0], pos[1])); + } catch (e) { + base = null; + } + var dx = base ? placer.place(base, i) : 0; + + var html ='
' + (n.img ? '' : '' + n.icon + '') + '' + (i + 1) + '' + '
'; - var marker = new AMap.Marker({ + var opts = { position: pos, content: html, anchor: 'center', map: map, - }); + }; + /* 撞上了就整体往旁边挪 —— 用像素偏移,谁也不挡谁 */ + if (dx !== 0) { + opts.offset = new AMap.Pixel(dx, 0); + } + var marker = new AMap.Marker(opts); marker.on('click', function () { window.location.href = n.url; }); overlays.push(marker); } - /* 到达地小圆点(和出发地不是同一点时才画) */ + /* 到达地小圆点(和出发地不是同一点时才画)。 + 同一点位只画一个:往返行程两端的红点本来就该是同一个。 */ if (en && (!s || en.lng !== s.lng || en.lat !== s.lat)) { - var dot = new AMap.Marker({ - position: [en.lng, en.lat], - content: '
', - anchor: 'center', - map: map, - }); - dot.on('click', function () { window.location.href = n.url; }); - overlays.push(dot); + var dk = posKey([en.lng, en.lat]); + if (!dotSeen[dk]) { + dotSeen[dk] = 1; + var dot = new AMap.Marker({ + position: [en.lng, en.lat], + content: '
', + anchor: 'center', + map: map, + }); + dot.on('click', function () { window.location.href = n.url; }); + overlays.push(dot); + } } prevEnd = en || s; @@ -235,9 +335,12 @@ require dirname(__DIR__) . '/comments/_block.php'; strokeStyle: 'dashed', strokeOpacity: 0.8, map: map, })); } - if (overlays.length) map.setFitView(null, false, [50, 50, 50, 50]); + if (overlays.length && !fromZoom) map.setFitView(null, false, [50, 50, 50, 50]); } drawRoute(); + /* 缩放会改变标记之间的屏幕距离:缩小时原本分开的标记可能又叠在一起, + 所以缩放结束后按新的像素位置重排一次(不重新适配视野)。 */ + map.on('zoomend', function () { drawRoute(true); }); /* 只有地名、没有坐标的地点:向高德地理编码查坐标(插件按需加载)。 查到就补进数据并重画;查不到(地名太偏/写错)保持缺省,不硬造位置。 */