// map-script.js class WorldMap { constructor() { // 示例地图数据 this.defaultMapData = { places: ["Unknown"], distances: [ ] }; this.mapData = null; this.selectedNode = null; this.svg = null; this.simulation = null; this.container = null; this.width = 0; this.height = 0; this.init(); } init() { document.addEventListener('DOMContentLoaded', () => { // 初始化地图容器 this.initMapContainer(); // 监听窗口调整 window.addEventListener('resize', () => this.handleResize()); // 先使用默认数据 this.updateMap(this.defaultMapData); // 监听WebSocket消息 window.addEventListener('websocket-message', (event) => { const message = event.detail; if (message.type === 'initial_data' && message.data.map) { this.updateMap(message.data.map); } }); }); } initMapContainer() { const container = document.getElementById('map-container'); this.width = container.clientWidth; this.height = container.clientHeight; // 创建缩放行为 const zoom = d3.zoom() .scaleExtent([0.8, 2]) .on("zoom", (event) => this.zoomed(event)); // 创建SVG画布 this.svg = d3.select("#map") .append("svg") .attr("width", this.width) .attr("height", this.height) .call(zoom); // 创建背景层 const backgroundLayer = this.svg.append("g") .attr("class", "background-layer"); // 加载背景图片 this.loadBackgroundImage("./frontend/assets/images/fantasy-map.png", backgroundLayer); // 创建容器 this.container = this.svg.append("g") .attr("class", "nodes-container"); } updateMap(mapData) { this.mapData = mapData; // 准备数据 const nodes = mapData.places.map(place => ({id: place})); const links = mapData.distances.map(d => ({ source: d.source, target: d.target, distance: d.distance })); // 重置容器 this.container.selectAll("*").remove(); // 创建力导向图 this.simulation = d3.forceSimulation() .force("link", d3.forceLink().id(d => d.id).distance(d => d.distance * 5)) .force("charge", d3.forceManyBody().strength(-2000)) .force("center", d3.forceCenter(this.width / 2, this.height / 2)) .force("collision", d3.forceCollide().radius(40)); // 创建连线和节点 this.createLinks(links); this.createNodes(nodes); // 更新力导向图 this.simulation .nodes(nodes) .on("tick", () => this.ticked()); this.simulation.force("link") .links(links); } zoomed(event) { this.container.attr("transform", event.transform); } createLinks(links) { // 创建连线 this.link = this.container.append("g") .selectAll("line") .data(links) .enter() .append("line") .attr("class", "link"); // 创建距离标签 this.distanceLabels = this.container.append("g") .selectAll("text") .data(links) .enter() .append("text") .attr("class", "distance-label") .text(d => d.distance) .attr("stroke", "white") .attr("stroke-width", "2px") .attr("paint-order", "stroke"); } createNodes(nodes) { // 创建节点组 this.node = this.container.append("g") .selectAll(".node") .data(nodes) .enter() .append("g") .attr("class", "node") .call(d3.drag() .on("start", (event, d) => this.dragstarted(event, d)) .on("drag", (event, d) => this.dragged(event, d)) .on("end", (event, d) => this.dragended(event, d))) .on("click", (event, d) => this.handleNodeClick(event, d)); // 添加节点圆形 this.node.append("circle") .attr("r", 20); // 添加节点文本 this.node.append("text") .attr("text-anchor", "middle") .attr("dominant-baseline", "middle") .text(d => this.formatNodeName(d.id)); this.node.append("title") .text(d => d.id); // 创建弹出框 if (!this.popup) { this.popup = d3.select("body") .append("div") .attr("class", "popup") .style("opacity", 0); } // 添加全局点击事件 d3.select("body").on("click", () => { if (this.selectedNode) { this.deselectNode(); } }); } ticked() { this.link .attr("x1", d => d.source.x) .attr("y1", d => d.source.y) .attr("x2", d => d.target.x) .attr("y2", d => d.target.y); this.distanceLabels .attr("x", d => (d.source.x + d.target.x) / 2) .attr("y", d => (d.source.y + d.target.y) / 2); this.node .attr("transform", d => `translate(${d.x},${d.y})`); } dragstarted(event, d) { if (!event.active) this.simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } dragged(event, d) { const transform = d3.zoomTransform(this.svg.node()); d.fx = (event.x - transform.x) / transform.k; d.fy = (event.y - transform.y) / transform.k; } dragended(event, d) { if (!event.active) this.simulation.alphaTarget(0); d.fx = null; d.fy = null; } handleNodeClick(event, d) { event.stopPropagation(); if (this.selectedNode === event.currentTarget) { this.deselectNode(); } else { if (this.selectedNode) { this.deselectNode(); } this.selectedNode = event.currentTarget; d3.select(this.selectedNode) .select("circle") .classed("selected", true) .transition() .duration(200) .attr("r", 30); this.link.transition() .duration(200) .style("stroke-opacity", l => (l.source.id === d.id || l.target.id === d.id) ? 1 : 0.2 ) .style("stroke", l => (l.source.id === d.id || l.target.id === d.id) ? "#ff0000" : "#999" ); this.popup.transition() .duration(200) .style("opacity", .9); this.popup.html(`
连接数: ${this.getConnectedLinks(d.id).length}
相邻节点: ${this.getConnectedNodes(d.id).join(", ")}
`) .style("left", (event.pageX + 10) + "px") .style("top", (event.pageY - 10) + "px"); } } deselectNode() { d3.select(this.selectedNode) .select("circle") .classed("selected", false) .transition() .duration(200) .attr("r", 20); this.link.transition() .duration(200) .style("stroke-opacity", 0.6) .style("stroke", "#999"); this.popup.transition() .duration(200) .style("opacity", 0); this.selectedNode = null; } formatNodeName(name, maxLength = 3) { if (name.length <= maxLength) return name; if (/^[\u4e00-\u9fa5]+$/.test(name)) { return name.substring(0, maxLength - 1) + '…'; } else { return name.substring(0, maxLength) + '...'; } } getConnectedNodes(nodeId) { return this.mapData.distances .filter(l => l.source === nodeId || l.target === nodeId) .map(l => l.source === nodeId ? l.target : l.source); } getConnectedLinks(nodeId) { return this.mapData.distances .filter(l => l.source === nodeId || l.target === nodeId); } loadBackgroundImage(url, backgroundLayer) { const img = new Image(); img.onload = () => { const background = this.svg.append("image") .attr("class", "map-background") .attr("xlink:href", url) .attr("width", this.width) .attr("height", this.height); backgroundLayer.append(() => background.node()); }; img.onerror = () => { backgroundLayer.append("rect") .attr("width", this.width) .attr("height", this.height) .attr("fill", "#f0f0f0"); }; img.src = url; } handleResize() { const container = document.getElementById('map-container'); this.width = container.clientWidth; this.height = container.clientHeight; this.svg .attr('width', this.width) .attr('height', this.height); this.simulation.force('center', d3.forceCenter(this.width / 2, this.height / 2)); this.simulation.alpha(0.3).restart(); } } const worldMap = new WorldMap();