refactor(web): 优化线路图走线为45度和直角转折,并在起终点显示线路名
This commit is contained in:
@@ -16,8 +16,8 @@
|
|||||||
"mtimeMs": 1783602619602.658
|
"mtimeMs": 1783602619602.658
|
||||||
},
|
},
|
||||||
"ticket-order.js": {
|
"ticket-order.js": {
|
||||||
"size": 37866,
|
"size": 37477,
|
||||||
"mtimeMs": 1783818082616.8108
|
"mtimeMs": 1783820973442.9248
|
||||||
},
|
},
|
||||||
"ticket-search.js": {
|
"ticket-search.js": {
|
||||||
"size": 17269,
|
"size": 17269,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"blog.css": 2,
|
"blog.css": 2,
|
||||||
"custom-dialog.js": 11,
|
"custom-dialog.js": 11,
|
||||||
"public-status.js": 14,
|
"public-status.js": 14,
|
||||||
"ticket-order.js": 24,
|
"ticket-order.js": 25,
|
||||||
"ticket-search.js": 12,
|
"ticket-search.js": 12,
|
||||||
"ticket-route.js": 3,
|
"ticket-route.js": 3,
|
||||||
"index.js": 5,
|
"index.js": 5,
|
||||||
|
|||||||
@@ -158,7 +158,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/custom-dialog.js?v=11"></script>
|
<script src="/custom-dialog.js?v=11"></script>
|
||||||
<script src="/ticket-order.js?v=24"></script>
|
<script src="/ticket-order.js?v=25"></script>
|
||||||
<script src="/public-status.js?v=14"></script>
|
<script src="/public-status.js?v=14"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|||||||
+104
-115
@@ -55,117 +55,87 @@
|
|||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
|
|
||||||
const appendPolylinePoint = (points, x, y) => {
|
const buildTransitPath = (points, radius = 12) => {
|
||||||
const px = Math.round(Number(x) || 0);
|
if (!points || points.length === 0) return '';
|
||||||
const py = Math.round(Number(y) || 0);
|
|
||||||
const last = points[points.length - 1];
|
|
||||||
if (last && last.x === px && last.y === py) return;
|
|
||||||
points.push({ x: px, y: py });
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildRoundedOrthogonalPath = (points, radius = 14) => {
|
|
||||||
if (!Array.isArray(points) || points.length === 0) return '';
|
|
||||||
if (points.length === 1) return `M ${points[0].x} ${points[0].y}`;
|
if (points.length === 1) return `M ${points[0].x} ${points[0].y}`;
|
||||||
|
|
||||||
let d = `M ${points[0].x} ${points[0].y}`;
|
const expanded = [points[0]];
|
||||||
for (let i = 1; i < points.length; i++) {
|
for (let i = 1; i < points.length; i++) {
|
||||||
const prev = points[i - 1];
|
const prev = points[i - 1];
|
||||||
const cur = points[i];
|
const cur = points[i];
|
||||||
const next = points[i + 1];
|
const dx = cur.x - prev.x;
|
||||||
|
const dy = cur.y - prev.y;
|
||||||
|
|
||||||
|
if (dx === 0 || dy === 0 || Math.abs(dx) === Math.abs(dy)) {
|
||||||
|
expanded.push(cur);
|
||||||
|
} else {
|
||||||
|
const absDx = Math.abs(dx);
|
||||||
|
const absDy = Math.abs(dy);
|
||||||
|
if (absDx > absDy) {
|
||||||
|
const signX = Math.sign(dx);
|
||||||
|
const straightLen = absDx - absDy;
|
||||||
|
expanded.push({ x: prev.x + signX * (straightLen / 2), y: prev.y });
|
||||||
|
expanded.push({ x: cur.x - signX * (straightLen / 2), y: cur.y });
|
||||||
|
expanded.push(cur);
|
||||||
|
} else {
|
||||||
|
const signY = Math.sign(dy);
|
||||||
|
const straightLen = absDy - absDx;
|
||||||
|
expanded.push({ x: prev.x, y: prev.y + signY * (straightLen / 2) });
|
||||||
|
expanded.push({ x: cur.x, y: cur.y - signY * (straightLen / 2) });
|
||||||
|
expanded.push(cur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleaned = [expanded[0]];
|
||||||
|
for (let i = 1; i < expanded.length; i++) {
|
||||||
|
const last = cleaned[cleaned.length - 1];
|
||||||
|
const p = expanded[i];
|
||||||
|
if (Math.abs(last.x - p.x) > 0.1 || Math.abs(last.y - p.y) > 0.1) {
|
||||||
|
cleaned.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let d = `M ${cleaned[0].x} ${cleaned[0].y}`;
|
||||||
|
for (let i = 1; i < cleaned.length; i++) {
|
||||||
|
const prev = cleaned[i - 1];
|
||||||
|
const cur = cleaned[i];
|
||||||
|
const next = cleaned[i + 1];
|
||||||
|
|
||||||
if (!next) {
|
if (!next) {
|
||||||
d += ` L ${cur.x} ${cur.y}`;
|
d += ` L ${cur.x} ${cur.y}`;
|
||||||
continue;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inDx = cur.x - prev.x;
|
const dx1 = cur.x - prev.x;
|
||||||
const inDy = cur.y - prev.y;
|
const dy1 = cur.y - prev.y;
|
||||||
const outDx = next.x - cur.x;
|
const len1 = Math.sqrt(dx1*dx1 + dy1*dy1);
|
||||||
const outDy = next.y - cur.y;
|
const dx2 = next.x - cur.x;
|
||||||
const isCorner = ((inDx === 0 && outDy === 0) || (inDy === 0 && outDx === 0)) && !(inDx === 0 && outDx === 0) && !(inDy === 0 && outDy === 0);
|
const dy2 = next.y - cur.y;
|
||||||
if (!isCorner) {
|
const len2 = Math.sqrt(dx2*dx2 + dy2*dy2);
|
||||||
|
|
||||||
|
if (len1 < 0.1 || len2 < 0.1) {
|
||||||
d += ` L ${cur.x} ${cur.y}`;
|
d += ` L ${cur.x} ${cur.y}`;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const inLen = Math.abs(inDx) + Math.abs(inDy);
|
const cross = dx1 * dy2 - dy1 * dx2;
|
||||||
const outLen = Math.abs(outDx) + Math.abs(outDy);
|
if (Math.abs(cross) < 0.1) {
|
||||||
const r = Math.max(0, Math.min(radius, inLen / 2, outLen / 2));
|
d += ` L ${cur.x} ${cur.y}`;
|
||||||
const inX = cur.x - Math.sign(inDx) * r;
|
continue;
|
||||||
const inY = cur.y - Math.sign(inDy) * r;
|
}
|
||||||
const outX = cur.x + Math.sign(outDx) * r;
|
|
||||||
const outY = cur.y + Math.sign(outDy) * r;
|
const r = Math.min(radius, len1 / 2, len2 / 2);
|
||||||
d += ` L ${inX} ${inY} Q ${cur.x} ${cur.y} ${outX} ${outY}`;
|
const startX = cur.x - (dx1 / len1) * r;
|
||||||
|
const startY = cur.y - (dy1 / len1) * r;
|
||||||
|
const endX = cur.x + (dx2 / len2) * r;
|
||||||
|
const endY = cur.y + (dy2 / len2) * r;
|
||||||
|
|
||||||
|
d += ` L ${startX} ${startY} Q ${cur.x} ${cur.y} ${endX} ${endY}`;
|
||||||
}
|
}
|
||||||
return d;
|
return d;
|
||||||
};
|
};
|
||||||
|
|
||||||
function isTransferCode(code) {
|
|
||||||
const normalized = String(code || '').trim();
|
|
||||||
if (!normalized) return false;
|
|
||||||
const canonical = stationCanonicalByCode[normalized] || normalized;
|
|
||||||
const codes = stationCodesByCanonical[canonical] || [];
|
|
||||||
return codes.length >= 2 || stationTransfer.has(normalized);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildLinePath(points, yLine) {
|
|
||||||
if (!Array.isArray(points) || points.length === 0) return '';
|
|
||||||
const poly = [];
|
|
||||||
appendPolylinePoint(poly, points[0].x, points[0].y);
|
|
||||||
for (let i = 1; i < points.length; i++) {
|
|
||||||
const prev = points[i - 1];
|
|
||||||
const cur = points[i];
|
|
||||||
const prevOnLine = Math.abs(prev.y - yLine) < 0.5;
|
|
||||||
const curOnLine = Math.abs(cur.y - yLine) < 0.5;
|
|
||||||
|
|
||||||
if (prev.x === cur.x || prev.y === cur.y) {
|
|
||||||
appendPolylinePoint(poly, cur.x, cur.y);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevOnLine && curOnLine) {
|
|
||||||
appendPolylinePoint(poly, cur.x, cur.y);
|
|
||||||
} else if (prevOnLine && !curOnLine) {
|
|
||||||
appendPolylinePoint(poly, cur.x, yLine);
|
|
||||||
appendPolylinePoint(poly, cur.x, cur.y);
|
|
||||||
} else if (!prevOnLine && curOnLine) {
|
|
||||||
appendPolylinePoint(poly, prev.x, yLine);
|
|
||||||
appendPolylinePoint(poly, cur.x, cur.y);
|
|
||||||
} else {
|
|
||||||
appendPolylinePoint(poly, prev.x, yLine);
|
|
||||||
appendPolylinePoint(poly, cur.x, yLine);
|
|
||||||
appendPolylinePoint(poly, cur.x, cur.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return buildRoundedOrthogonalPath(poly, 14);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildRoutePath(points, codes) {
|
|
||||||
if (!Array.isArray(points) || points.length === 0) return '';
|
|
||||||
const poly = [];
|
|
||||||
appendPolylinePoint(poly, points[0].x, points[0].y);
|
|
||||||
for (let i = 1; i < points.length; i++) {
|
|
||||||
const prev = points[i - 1];
|
|
||||||
const cur = points[i];
|
|
||||||
const prevTransfer = isTransferCode(codes?.[i - 1]);
|
|
||||||
const curTransfer = isTransferCode(codes?.[i]);
|
|
||||||
|
|
||||||
if (prev.x === cur.x || prev.y === cur.y) {
|
|
||||||
appendPolylinePoint(poly, cur.x, cur.y);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!prevTransfer && curTransfer) {
|
|
||||||
appendPolylinePoint(poly, cur.x, prev.y);
|
|
||||||
} else if (prevTransfer && !curTransfer) {
|
|
||||||
appendPolylinePoint(poly, prev.x, cur.y);
|
|
||||||
} else {
|
|
||||||
appendPolylinePoint(poly, prev.x, cur.y);
|
|
||||||
}
|
|
||||||
appendPolylinePoint(poly, cur.x, cur.y);
|
|
||||||
}
|
|
||||||
return buildRoundedOrthogonalPath(poly, 16);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadMap() {
|
async function loadMap() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/public/fares/map/light');
|
const res = await fetch('/api/public/fares/map/light');
|
||||||
@@ -291,12 +261,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build SVG
|
// Build SVG
|
||||||
const legendW = 210;
|
const legendW = 10;
|
||||||
const legendX = 16;
|
const startX = 140;
|
||||||
const legendSwatchW = 22;
|
const baseY = 80;
|
||||||
const legendTextX = legendX + legendSwatchW + 10;
|
|
||||||
const startX = legendW + 90;
|
|
||||||
const baseY = 44;
|
|
||||||
const lineGapY = 92;
|
const lineGapY = 92;
|
||||||
const minGapX = 78;
|
const minGapX = 78;
|
||||||
|
|
||||||
@@ -364,28 +331,48 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let svgContent = '';
|
const labelPlacements = {};
|
||||||
const drawnLegends = new Set();
|
const placeLabel = (x, y, isStart, name, color) => {
|
||||||
|
const key = `${x},${y},${isStart ? 'left' : 'right'}`;
|
||||||
|
const count = labelPlacements[key] || 0;
|
||||||
|
labelPlacements[key] = count + 1;
|
||||||
|
|
||||||
for (const li of lineStops) {
|
const nameLen = name.length * 13 + 16;
|
||||||
const yLine = baseY + li.idx * lineGapY;
|
const yOffset = count * 30 - 12; // stack vertically, center on point initially
|
||||||
|
|
||||||
if (!drawnLegends.has(li.idx)) {
|
let lx, textX;
|
||||||
svgContent += `<rect x="${legendX}" y="${yLine-10}" width="${legendSwatchW}" height="10" rx="5" fill="${li.color}" />`;
|
if (isStart) {
|
||||||
svgContent += `<text x="${legendTextX}" y="${yLine-1}" fill="#e5e7eb" font-size="16" font-weight="800">${li.name}</text>`;
|
lx = x - 28 - nameLen;
|
||||||
drawnLegends.add(li.idx);
|
textX = lx + nameLen / 2;
|
||||||
|
} else {
|
||||||
|
lx = x + 28;
|
||||||
|
textX = lx + nameLen / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return `<rect x="${lx}" y="${y + yOffset}" width="${nameLen}" height="24" rx="12" fill="${color}" />` +
|
||||||
|
`<text x="${textX}" y="${y + yOffset + 16}" text-anchor="middle" fill="#fff" font-size="12" font-weight="bold">${name}</text>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
let svgContent = '';
|
||||||
|
|
||||||
|
for (const li of lineStops) {
|
||||||
if (li.stops.length >= 2) {
|
if (li.stops.length >= 2) {
|
||||||
const pts = li.stops.map(code => {
|
const pts = li.stops.map(code => {
|
||||||
const canonical = stationCanonicalByCode[code] || code;
|
const canonical = stationCanonicalByCode[code] || code;
|
||||||
return {
|
return {
|
||||||
x: stationXByCanonical[canonical] || 0,
|
x: stationXByCanonical[canonical] || 0,
|
||||||
y: stationYByCanonical[code] ?? yLine
|
y: stationYByCanonical[code] ?? (baseY + li.idx * lineGapY)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const d = buildLinePath(pts, yLine);
|
|
||||||
|
const d = buildTransitPath(pts, 16);
|
||||||
svgContent += `<path d="${d}" fill="none" stroke="${li.color}" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" />`;
|
svgContent += `<path d="${d}" fill="none" stroke="${li.color}" stroke-width="8" stroke-linecap="round" stroke-linejoin="round" />`;
|
||||||
|
|
||||||
|
// Add line name labels at ends
|
||||||
|
const pStart = pts[0];
|
||||||
|
const pEnd = pts[pts.length - 1];
|
||||||
|
svgContent += placeLabel(pStart.x, pStart.y, true, li.name, li.color);
|
||||||
|
svgContent += placeLabel(pEnd.x, pEnd.y, false, li.name, li.color);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,7 +400,7 @@
|
|||||||
markerSvg += `<circle class="node-core" cx="${x}" cy="${y}" r="7" fill="#ffffff" stroke="#334155" stroke-width="2.25" />`;
|
markerSvg += `<circle class="node-core" cx="${x}" cy="${y}" r="7" fill="#ffffff" stroke="#334155" stroke-width="2.25" />`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const preferTop = isTransfer || (i % 2 === 0 && y > baseY + 20);
|
const preferTop = isTransfer || (i % 2 === 0 && y >= baseY + 20);
|
||||||
const labelY = preferTop ? (y - 22) : (y + 30);
|
const labelY = preferTop ? (y - 22) : (y + 30);
|
||||||
const labelClass = preferTop ? 'station-label-top' : 'station-label-bottom';
|
const labelClass = preferTop ? 'station-label-top' : 'station-label-bottom';
|
||||||
let labelSvg = '';
|
let labelSvg = '';
|
||||||
@@ -436,7 +423,7 @@
|
|||||||
|
|
||||||
const allX = Object.values(stationXByCanonical);
|
const allX = Object.values(stationXByCanonical);
|
||||||
const maxX = allX.length > 0 ? Math.max(...allX) : 0;
|
const maxX = allX.length > 0 ? Math.max(...allX) : 0;
|
||||||
const width = Math.max(560, maxX + 120);
|
const width = Math.max(800, maxX + 180);
|
||||||
const height = Math.max(260, baseY + currentLineIdx * lineGapY + 60);
|
const height = Math.max(260, baseY + currentLineIdx * lineGapY + 60);
|
||||||
mapContainer.innerHTML = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${svgContent}</svg>`;
|
mapContainer.innerHTML = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">${svgContent}</svg>`;
|
||||||
|
|
||||||
@@ -603,7 +590,7 @@
|
|||||||
const group = document.createElementNS(ns, 'g');
|
const group = document.createElementNS(ns, 'g');
|
||||||
group.setAttribute('class', 'route-overlay-group');
|
group.setAttribute('class', 'route-overlay-group');
|
||||||
|
|
||||||
const pathData = buildRoutePath(points, currentRoute);
|
const pathData = buildTransitPath(points, 16);
|
||||||
if (!pathData) return;
|
if (!pathData) return;
|
||||||
|
|
||||||
const glow = document.createElementNS(ns, 'path');
|
const glow = document.createElementNS(ns, 'path');
|
||||||
@@ -633,9 +620,9 @@
|
|||||||
totalDist += Math.sqrt(dx * dx + dy * dy);
|
totalDist += Math.sqrt(dx * dx + dy * dy);
|
||||||
}
|
}
|
||||||
|
|
||||||
const speed = 100; // pixels per second
|
const speed = 150; // pixels per second
|
||||||
const duration = Math.max(1, totalDist / speed);
|
const duration = Math.max(1, totalDist / speed);
|
||||||
const numArrows = Math.max(1, Math.floor(duration / 0.7)); // one arrow approx every 0.7s
|
const numArrows = Math.max(1, Math.floor(duration / 0.8));
|
||||||
const interval = duration / numArrows;
|
const interval = duration / numArrows;
|
||||||
|
|
||||||
for (let i = 0; i < numArrows; i++) {
|
for (let i = 0; i < numArrows; i++) {
|
||||||
@@ -655,6 +642,8 @@
|
|||||||
animateMotion.setAttribute('path', pathData);
|
animateMotion.setAttribute('path', pathData);
|
||||||
animateMotion.setAttribute('rotate', 'auto');
|
animateMotion.setAttribute('rotate', 'auto');
|
||||||
animateMotion.setAttribute('begin', `-${i * interval}s`);
|
animateMotion.setAttribute('begin', `-${i * interval}s`);
|
||||||
|
// Use keyPoints/keyTimes to ensure constant speed along the path
|
||||||
|
animateMotion.setAttribute('calcMode', 'linear');
|
||||||
|
|
||||||
arrowG.appendChild(arrowPath);
|
arrowG.appendChild(arrowPath);
|
||||||
arrowG.appendChild(animateMotion);
|
arrowG.appendChild(animateMotion);
|
||||||
|
|||||||
Reference in New Issue
Block a user