help me use geoJSON + d3-geo + threejs to achieve 3d administrative map display on coplilot and cursor
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>3D Administrative Map</title>
<script src="./js/three.js"></script>
<script src="./js/OrbitControls.js"></script>
<script src="./js/d3-geo.v1.min.js"></script>
<style>
body {
margin: 0;
}
#webgl {
width: 100vw;
height: 100vh;
}
.tooltip {
position: absolute;
padding: 10px;
background: rgba(0, 0, 0, 0.7);
color: white;
border-radius: 4px;
font-size: 14px;
pointer-events: none;
display: none;
}
</style>
</head>
<body>
cursor
<div id="webgl"></div>
<div class="tooltip"></div>
<script>
// 初始化场景、相机和渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.getElementById('webgl').appendChild(renderer.domElement);
// 添加轨道控制器
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.05;
// 设置相机位置
camera.position.set(0, 0, 100);
// 添加环境光和平行光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.6);
directionalLight.position.set(10, 10, 10);
scene.add(directionalLight);
// 创建投影
const projection = d3.geoMercator()
.center([113.280637, 23.125178])
.scale(100)
.translate([0, 0]);
// 用于鼠标拾取
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const tooltip = document.querySelector('.tooltip');
// 颜色数组
const colors = [
0x66c2a5, 0xfc8d62, 0x8da0cb, 0xe78ac3,
0xa6d854, 0xffd92f, 0xe5c494, 0xb3b3b3
];
// 加载GeoJSON数据
const loader = new THREE.FileLoader();
const meshes = new Map(); // 存储mesh和对应的区域信息
// loader.load('./json/gz.json', function(data) {
loader.load('./json/china.json', function (data) {
const json = JSON.parse(data);
json.features.forEach((feature, index) => {
if (feature.geometry) {
const shape = new THREE.Shape();
let isFirst = true;
// 处理多边形数据
feature.geometry.coordinates[0][0].forEach(coord => {
const [x, y] = projection(coord);
if (isFirst) {
shape.moveTo(x, y);
isFirst = false;
} else {
shape.lineTo(x, y);
}
});
// 随机生成区域高度(实际项目中可以根据真实数据设置)
const height = Math.random() * 5 + 1;
// 创建拉伸几何体
const extrudeSettings = {
depth: height,
bevelEnabled: false
};
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
// 创建材质
const material = new THREE.MeshPhongMaterial({
color: colors[index % colors.length],
transparent: true,
opacity: 0.8,
side: THREE.DoubleSide
});
// 创建网格并添加到场景
const mesh = new THREE.Mesh(geometry, material);
mesh.rotation.x = Math.PI;
// 存储原始颜色
mesh.userData.originalColor = material.color.getHex();
mesh.userData.name = feature.properties.name || `区域 ${index + 1}`;
mesh.userData.value = height.toFixed(1); // 存储随机生成的高度值
meshes.set(mesh.id, mesh);
scene.add(mesh);
// 添加文字标签(这里使用简单的HTML元素作为标签)
const [labelX, labelY] = projection(feature.geometry.coordinates[0][0][0]);
const label = document.createElement('div');
label.className = 'map-label';
label.style.position = 'absolute';
label.style.color = 'white';
label.style.fontSize = '12px';
label.textContent = mesh.userData.name;
document.body.appendChild(label);
}
});
// 渲染循环
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
});
// 鼠标移动事件处理
function onMouseMove(event) {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(scene.children);
// 重置所有mesh的颜色
meshes.forEach(mesh => {
mesh.material.color.setHex(mesh.userData.originalColor);
});
if (intersects.length > 0) {
const intersected = intersects[0].object;
// 高亮选中的区域
intersected.material.color.setHex(0xff0000);
// 显示工具提示
tooltip.style.display = 'block';
tooltip.style.left = event.clientX + 10 + 'px';
tooltip.style.top = event.clientY + 10 + 'px';
tooltip.innerHTML = `
区域:${intersected.userData.name}<br>
数值:${intersected.userData.value}
`;
} else {
tooltip.style.display = 'none';
}
}
// 窗口大小调整处理
window.addEventListener('resize', onWindowResize, false);
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// 添加鼠标事件监听
window.addEventListener('mousemove', onMouseMove, false);
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>3D 行政区划地图展示</title>
<!-- 引入 Three.js、OrbitControls、d3‐geo -->
<script src="./js/three.js"></script>
<script src="./js/OrbitControls.js"></script>
<script src="./js/d3-geo.v1.min.js"></script>
<style>
/* 让 #webgl 占满全屏 */
body,
html {
margin: 0;
overflow: hidden;
width: 100%;
height: 100%;
}
#webgl {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
/* tooltip 样式 */
.tooltip {
position: absolute;
pointer-events: none;
padding: 4px 8px;
background-color: rgba(0, 0, 0, 0.7);
color: #fff;
font-size: 13px;
border-radius: 3px;
white-space: nowrap;
display: none;
transform: translate(-50%, -120%);
z-index: 10;
}
</style>
</head>
<body>
chatgpt
<!-- Three.js 渲染容器 -->
<div id="webgl"></div>
<!-- 悬浮在页面上,用来显示省名 -->
<div class="tooltip"></div>
<script>
// —— 一、基础场景初始化 ——
const container = document.getElementById('webgl');
const tooltip = document.querySelector('.tooltip');
// 场景、相机、渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
5000
);
// 将相机稍微抬高并倾斜,居中看向 (0,0,0)
// camera.position.set(0, -800, 600);
camera.position.set(0, 0, 1500);
camera.lookAt(scene.position);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
// OrbitControls
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.1;
controls.rotateSpeed = 0.6;
controls.minDistance = 200;
controls.maxDistance = 2000;
// 环境光 + 方向光
const ambientLight = new THREE.AmbientLight(0x888888);
scene.add(ambientLight);
const dirLight = new THREE.DirectionalLight(0xffffff, 0.6);
dirLight.position.set(100, -200, 300);
scene.add(dirLight);
// Raycaster 用于拾取
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
// 窗口大小变化时,更新 renderer 和 camera
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// —— 二、定义 D3 投影 ——
// 这里使用“墨卡托投影” (geoMercator)。中心坐标设为中国大致中心 (104E, 35N)。scale 数值可调。
const projection = d3.geoMercator()
.center([104, 35])
.scale(800) // 缩放比例:可根据 GeoJSON 范围做微调
.translate([0, 0]); // translate(0,0),因为后面我们不做 DOM 渲染,只读投影值
// —— 三、加载 GeoJSON 并构造几何体 ——
const loader = new THREE.FileLoader();
loader.load(
'./json/china.json',
(data) => {
const geojson = JSON.parse(data);
addGeoJSONToScene(geojson);
},
undefined,
(err) => {
console.error('GeoJSON 加载错误:', err);
}
);
/**
* 遍历 geojson.features,把每个省份挤压成 3D 形状加入场景
* @param {Object} geojson 整个 GeoJSON 对象
*/
function addGeoJSONToScene(geojson) {
const features = geojson.features;
// 遍历每个要素(省/市)
features.forEach((feature) => {
const prop = feature.properties || {};
const name = prop.name || prop.NAME || '未知地区'; // 常见属性字段:name、NAME 等
// 不同类型做不同处理:Polygon / MultiPolygon
if (feature.geometry.type === 'Polygon') {
const lst = [feature.geometry.coordinates];
buildPolygon(lst, name);
} else if (feature.geometry.type === 'MultiPolygon') {
buildPolygon(feature.geometry.coordinates, name);
}
});
}
/**
* 传入一组 polygon 数组 (可能包含多个 ring),把所有环都做成 THREE.Shape 并挤压
* @param {Array} polygons 格式为 [ [ [ [lon, lat], ... ], [ /* hole? */
function buildPolygon(polygons, name) {
polygons.forEach((polygon) => {
// polygon 本身是一个“环的数组”,第一个环是外轮廓,后面可能是 hole(但大部分行政区通常没洞)
const outerRing = polygon[0]; // [ [lon, lat], [lon, lat], ... ]
// D3 投影:lon,lat → [x, y]
const shapePoints = outerRing.map((coord) => {
const [lon, lat] = coord;
const [x, y] = projection([lon, lat]);
// Three.js 中 Y 轴默认朝上,D3 投影的 y 轴向下,所以这里把 y 取反
return new THREE.Vector2(x, -y);
});
// 构造 Shape
const shape = new THREE.Shape(shapePoints);
// TODO: 如果需要在省内部做“挖洞”,可以参考 polygon.slice(1) 里的 hole 环,
// 并用 shape.holes.push(new THREE.Path(holePoints)) 来做。但这里暂不演示。
// 挤压设置:depth = 拉伸高度,bevelEnabled = false 保持直边
const extrudeSettings = {
depth: 8, // 挤压高度:可以根据需求自己调(8 个单位是示例)
bevelEnabled: false
};
// 生成几何体
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
// 给每个省份随机一个较为“柔和”的颜色,方便区分
const color = new THREE.Color(`hsl(${Math.random() * 360}, 50%, 60%)`);
const material = new THREE.MeshLambertMaterial({
color: color,
transparent: false,
opacity: 1.0,
side: THREE.DoubleSide
});
const mesh = new THREE.Mesh(geometry, material);
// 将每个省份名称存入 userData,供后续 tooltip 使用
mesh.userData.name = name;
// 为了让地图被相机居中,我们可以把 geometry 平移一下:让所有点的中心大致在 (0,0)
// 这里简化:不做额外平移,后面可以手动调相机或 OrbitControls 来看整个地图
scene.add(mesh);
});
}
// —— 四、交互:鼠标移动时用 Raycaster 拾取 ——
function onMouseMove(event) {
// 计算归一化设备坐标 (NDC):[-1,1]
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
// 拾取所有场景中的可见对象(如果你场景里只有省份 Mesh,也可直接 scene.children)
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
const picked = intersects[0].object;
const name = picked.userData.name || '未知区域';
// 在鼠标附近显示 tooltip
tooltip.style.display = 'block';
tooltip.innerText = name;
tooltip.style.left = event.clientX + 'px';
tooltip.style.top = event.clientY + 'px';
} else {
tooltip.style.display = 'none';
}
}
renderer.domElement.addEventListener('mousemove', onMouseMove, false);
// —— 五、渲染循环 ——
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>