|
@@ -0,0 +1,474 @@
|
|
|
+<script>
|
|
|
+import * as THREE from 'three'
|
|
|
+import {OrbitControls} from 'three/addons/controls/OrbitControls.js';
|
|
|
+import { Line2 } from 'three/addons/lines/Line2.js';
|
|
|
+import { LineMaterial } from 'three/addons/lines/LineMaterial.js';
|
|
|
+import { LineGeometry } from 'three/addons/lines/LineGeometry.js';
|
|
|
+import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
|
|
|
+import { Interaction } from 'three.interaction/src/index.js';
|
|
|
+import * as d3 from 'd3-geo'
|
|
|
+import gsap from 'gsap'
|
|
|
+import { onMounted, ref, watch } from 'vue';
|
|
|
+
|
|
|
+const BASE_URL = "/src/assets"
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'Map3DChart',
|
|
|
+ props: {
|
|
|
+ debugger: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false
|
|
|
+ },
|
|
|
+ qx: String,
|
|
|
+ mapHeatData: {
|
|
|
+ type: Array,
|
|
|
+ default: () => []
|
|
|
+ },
|
|
|
+ formatter: {
|
|
|
+ type: Function,
|
|
|
+ default: function (params) {
|
|
|
+ let res = params.name + "<br/>";
|
|
|
+ res += `隐患<span style="font-size: 24px;color:red;font-weight:bold;">${params.value}</span>个`;
|
|
|
+ return res;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ areaColor: {
|
|
|
+ type: Function,
|
|
|
+ default: function (params) {
|
|
|
+ return params ? params.meta.分区颜色 : ''
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ setup(props, ctx) {
|
|
|
+ // 初始化3D地图
|
|
|
+ const mapTestBox = ref()
|
|
|
+ const activeArea = ref("")
|
|
|
+ let container = null;
|
|
|
+ let renderer = null, scene = null, camera = null ,labelRenderer = null, controls = null;
|
|
|
+ let WIDTH = 0, HEIGHT = 0;
|
|
|
+ let selectMesh = null;
|
|
|
+ let prevMesh = null;
|
|
|
+ let selectName = null;
|
|
|
+ let selectPrevColor = null;
|
|
|
+ var uniforms2 = {
|
|
|
+ u_time: { value: 0.0 }
|
|
|
+ };
|
|
|
+ var map = new THREE.Group();
|
|
|
+ const COLOR_MAP = {
|
|
|
+ "红色": "#ff4540",
|
|
|
+ "橙色": "#ff8934",
|
|
|
+ "黄色": "#ffff55",
|
|
|
+ "绿色": "#69ff90",
|
|
|
+ "蓝色": "#23a3ff"
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ let textureLoader = new THREE.TextureLoader(); //纹理贴图加载器
|
|
|
+ const cacheMap = new Map()
|
|
|
+ // const isInnerClick = ref(true)
|
|
|
+ const mapData = ref(getHeatMapData(props.mapHeatData))
|
|
|
+ function getHeatMapData(val) {
|
|
|
+ return val.reduce((config, current) => {
|
|
|
+ return {
|
|
|
+ ...config,
|
|
|
+ [current.name]: current
|
|
|
+ }
|
|
|
+ },{})
|
|
|
+ }
|
|
|
+
|
|
|
+ function getAreaColor(name) {
|
|
|
+ return COLOR_MAP[props.areaColor(mapData.value[name])]
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ watch(() => props.mapHeatData, (val) => {
|
|
|
+ mapData.value = getHeatMapData(val)
|
|
|
+
|
|
|
+ Object.keys( mapData.value ).forEach(key => {
|
|
|
+ const area = cacheMap.get(key);
|
|
|
+ if (area) {
|
|
|
+ area.mesh.material[0].color.set(COLOR_MAP[props.areaColor(mapData.value[key])])
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ }, {
|
|
|
+ deep: true
|
|
|
+ })
|
|
|
+ watch(() => props.qx, (val) => {
|
|
|
+ const area = cacheMap.get(val);
|
|
|
+ if (area) {
|
|
|
+ clickArea(area.mesh, area.name)
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+
|
|
|
+ onMounted(() => {
|
|
|
+ init()
|
|
|
+ })
|
|
|
+
|
|
|
+ function init() {
|
|
|
+ container = mapTestBox.value || document.body;
|
|
|
+ WIDTH = container.clientWidth
|
|
|
+ HEIGHT = container.clientHeight
|
|
|
+ initScene();
|
|
|
+ initCamera();
|
|
|
+ initRenderer();
|
|
|
+ initLights();
|
|
|
+ initControls();
|
|
|
+ new Interaction(renderer, scene, camera);
|
|
|
+ initGeoJson();
|
|
|
+ initDebugger();
|
|
|
+ // 添加物体
|
|
|
+ // const cubeGeometry = new THREE.BoxGeometry(1,1,1)
|
|
|
+ // const material = new THREE.MeshBasicMaterial({
|
|
|
+ // color: '#ffff00'
|
|
|
+ // })
|
|
|
+ // // 根据几何体和材质创建物体
|
|
|
+ // const cube =new THREE.Mesh(cubeGeometry, material)
|
|
|
+ // // 将几何体创建到场景当中
|
|
|
+ // scene.add(cube)
|
|
|
+ }
|
|
|
+
|
|
|
+ function initScene() {
|
|
|
+ // 1、创建场景
|
|
|
+ scene = new THREE.Scene()
|
|
|
+ }
|
|
|
+ // 初始化灯光
|
|
|
+ function initLights() {
|
|
|
+ scene.add(new THREE.AmbientLight(0xffffff,1.2));
|
|
|
+ let directionalLight1 = new THREE.DirectionalLight(0xffffff,1); //037af1
|
|
|
+ directionalLight1.position.set(-100, 10, -100);
|
|
|
+ let directionalLight2 = new THREE.DirectionalLight(0xffffff, 1);
|
|
|
+ directionalLight2.position.set(100, 10, 100);
|
|
|
+ scene.add(directionalLight1);
|
|
|
+ scene.add(directionalLight2);
|
|
|
+ }
|
|
|
+ function initCamera() {
|
|
|
+ // 2、创建透视相机
|
|
|
+ camera = new THREE.PerspectiveCamera(
|
|
|
+ 45,
|
|
|
+ WIDTH / HEIGHT,
|
|
|
+ 0.1,
|
|
|
+ 1050
|
|
|
+ )
|
|
|
+ // 设置相机位置
|
|
|
+ camera.position.set(3.4, 118, 92)
|
|
|
+ camera.lookAt(scene.position);
|
|
|
+ // scene.add(camera);
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ function initDebugger() {
|
|
|
+ if (props.debugger) {
|
|
|
+ // 添加坐标辅助器
|
|
|
+ const axisHelper = new THREE.AxesHelper(10000)
|
|
|
+ scene.add(axisHelper)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ function initRenderer() {
|
|
|
+ // 初始化渲染器
|
|
|
+ renderer = new THREE.WebGLRenderer({antialias: true, alpha: true})
|
|
|
+ renderer.setPixelRatio(window.devicePixelRatio);
|
|
|
+ renderer.setSize(WIDTH, HEIGHT)
|
|
|
+ renderer.setClearColor(0x070b13, 1);
|
|
|
+ container.appendChild(renderer.domElement)
|
|
|
+ // 初始化CSS2DRenderer
|
|
|
+ labelRenderer = new CSS2DRenderer();
|
|
|
+ labelRenderer.setSize(WIDTH,HEIGHT);
|
|
|
+ labelRenderer.domElement.style.position = 'absolute';
|
|
|
+ labelRenderer.domElement.style.top = '0px';
|
|
|
+ labelRenderer.domElement.style.pointerEvents = 'none';
|
|
|
+ container.appendChild(labelRenderer.domElement );
|
|
|
+ }
|
|
|
+ function initControls() {
|
|
|
+ controls = new OrbitControls(camera, renderer.domElement)
|
|
|
+ // 如果使用animate方法时,将此函数删除
|
|
|
+ //controls.addEventListener( 'change', render );
|
|
|
+ // 使动画循环使用时阻尼或自转 意思是否有惯性
|
|
|
+ controls.enableDamping = true;
|
|
|
+ //动态阻尼系数 就是鼠标拖拽旋转灵敏度
|
|
|
+ //controls.dampingFactor = 0.25;
|
|
|
+ //是否可以缩放
|
|
|
+ controls.enableZoom = true;
|
|
|
+ //是否自动旋转
|
|
|
+ controls.autoRotate = false;
|
|
|
+ controls.autoRotateSpeed = 0.5;
|
|
|
+ //设置相机距离原点的最远距离
|
|
|
+ controls.minDistance = 1;
|
|
|
+ //设置相机距离原点的最远距离
|
|
|
+ controls.maxDistance = 2000;
|
|
|
+ // orbitcontrols.minPolarAngle = Math.PI / 180*10;
|
|
|
+ controls.maxPolarAngle = Math.PI / 180*75;//不然看到底部,超过90就看到底部了
|
|
|
+ //是否开启右键拖拽
|
|
|
+ controls.enablePan = true;
|
|
|
+ controls.update();
|
|
|
+ function render() {
|
|
|
+ renderer.render(scene, camera);
|
|
|
+ labelRenderer.render(scene, camera);
|
|
|
+ controls.update();
|
|
|
+ requestAnimationFrame(render);
|
|
|
+ }
|
|
|
+ render();
|
|
|
+ }
|
|
|
+ function initGeoJson(){
|
|
|
+ let loader = new THREE.FileLoader();
|
|
|
+ loader.load(`${BASE_URL}/json/cq.json`, function (data) {
|
|
|
+ let jsonData = JSON.parse(data);
|
|
|
+ initMap(jsonData);
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ function clickArea(mesh, name) {
|
|
|
+ if (selectMesh === null) {
|
|
|
+ selectPrevColor = mesh.material[0].color.getHexString();
|
|
|
+ gsap.to(mesh.position, {
|
|
|
+ y: 8,
|
|
|
+ duration: .3
|
|
|
+ });
|
|
|
+ gsap.to(name.element.style, {
|
|
|
+ fontSize: '30px',
|
|
|
+ duration: .3
|
|
|
+ })
|
|
|
+ selectMesh = mesh;
|
|
|
+ selectName = name
|
|
|
+ } else if (selectMesh === mesh) {
|
|
|
+ gsap.to(selectMesh.position, {
|
|
|
+ y: 1.5,
|
|
|
+ duration: .3
|
|
|
+ });
|
|
|
+ gsap.to(name.element.style, {
|
|
|
+ fontSize: '12px',
|
|
|
+ duration: .3
|
|
|
+ })
|
|
|
+ prevMesh = mesh;
|
|
|
+ selectMesh = null;
|
|
|
+ selectName = null;
|
|
|
+ } else {
|
|
|
+ gsap.to(selectMesh.position, {
|
|
|
+ y: 1.5,
|
|
|
+ duration: .3
|
|
|
+ });
|
|
|
+ gsap.to(mesh.position, {
|
|
|
+ y: 8,
|
|
|
+ duration: .3
|
|
|
+ })
|
|
|
+ if (selectName) {
|
|
|
+ gsap.to(selectName.element.style, {
|
|
|
+ fontSize: '12px',
|
|
|
+ duration: .3
|
|
|
+ })
|
|
|
+ }
|
|
|
+ gsap.to(name.element.style, {
|
|
|
+ fontSize: '30px',
|
|
|
+ duration: .3
|
|
|
+ })
|
|
|
+ prevMesh = selectMesh
|
|
|
+ selectMesh = mesh;
|
|
|
+ selectPrevColor = mesh.material[0].color.getHexString();
|
|
|
+ selectName = name
|
|
|
+ }
|
|
|
+ prevMesh && prevMesh.material[0].color.set(`#${selectPrevColor}`)
|
|
|
+ selectMesh && selectMesh.material[0].color.set('#ffffff')
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 根据json生成地图
|
|
|
+ * @param chinaJson
|
|
|
+ */
|
|
|
+ function initMap(chinaJson) {
|
|
|
+ let textureMap=textureLoader.load(`${BASE_URL}/images/map/map_gray.jpg`);
|
|
|
+ let texturedispMap=textureLoader.load(`${BASE_URL}/images/map/map-disp.jpg`);
|
|
|
+ let texturefxMap=textureLoader.load(`${BASE_URL}/images/map/map-fx.jpg`);
|
|
|
+ textureMap.wrapS = THREE.RepeatWrapping; //纹理水平方向的平铺方式
|
|
|
+ textureMap.wrapT = THREE.RepeatWrapping; //纹理垂直方向的平铺方式
|
|
|
+ textureMap.flipY = texturefxMap.flipY = false;
|
|
|
+ textureMap.rotation = texturefxMap.rotation = THREE.MathUtils.degToRad(45);
|
|
|
+ const scale = 0.01;
|
|
|
+ textureMap.repeat.set(scale, scale);
|
|
|
+ texturefxMap.repeat.set(scale, scale);
|
|
|
+ textureMap.offset.set(0.5,0.5);
|
|
|
+ texturefxMap.offset.set(0.5,0.5);
|
|
|
+ var matLine = new LineMaterial( {
|
|
|
+ color: 0xffffff,
|
|
|
+ linewidth: 0.0013,
|
|
|
+ vertexColors: true,
|
|
|
+ dashed: false,
|
|
|
+ alphaToCoverage: true,
|
|
|
+ } );
|
|
|
+
|
|
|
+ var matLine2 = new LineMaterial( {
|
|
|
+ color: '#01bdc2',
|
|
|
+ linewidth: 0.0025,
|
|
|
+ vertexColors: true,
|
|
|
+ dashed: false,
|
|
|
+ alphaToCoverage: true,
|
|
|
+ } );
|
|
|
+ // 新建一个省份容器:用来存放省份对应的模型和轮廓线
|
|
|
+ const meshArrs = new THREE.Object3D()
|
|
|
+ const meshs = []
|
|
|
+ cacheMap.clear()
|
|
|
+ // d3-geo转化坐标
|
|
|
+ const projection = d3.geoMercator().center([107.70084090820312, 29.942008602258]).scale(1740).translate([0, 0]);
|
|
|
+ // 遍历省份构建模型
|
|
|
+ chinaJson.features.forEach(elem => {
|
|
|
+ const province = new THREE.Object3D()
|
|
|
+ const coordinates = elem.geometry.coordinates
|
|
|
+ const properties = elem.properties;
|
|
|
+ //这里创建光柱
|
|
|
+ const [name,info] = initLightPoint(properties,projection)
|
|
|
+ coordinates.forEach(multiPolygon => {
|
|
|
+ multiPolygon.forEach(polygon => {
|
|
|
+ const positions = [];
|
|
|
+ var colors = [];
|
|
|
+ const color = new THREE.Color();
|
|
|
+ const shape = new THREE.Shape()
|
|
|
+ const extrudeSettings = {
|
|
|
+ depth: 2, //该属性指定图形可以拉伸多高,默认值是100
|
|
|
+ bevelEnabled: false, //是否给这个形状加斜面,默认加斜面。
|
|
|
+ };
|
|
|
+ var linGeometry = new LineGeometry();
|
|
|
+ for (let i = 0; i < polygon.length; i+=1) {
|
|
|
+ const [x, y] = projection(polygon[i])
|
|
|
+ positions.push(x, -y, 4.01);
|
|
|
+ color.setHSL( 1 , 1, 1 );
|
|
|
+ colors.push( color.r, color.g, color.b );
|
|
|
+ if (i === 0) {
|
|
|
+ shape.moveTo(x, -y)
|
|
|
+ }
|
|
|
+ shape.lineTo(x, -y);
|
|
|
+ }
|
|
|
+ const material = new THREE.MeshPhongMaterial({
|
|
|
+ map: textureMap,
|
|
|
+ displacementMap: texturedispMap,
|
|
|
+ displacementScale: .5,
|
|
|
+ color: getAreaColor(properties.name),
|
|
|
+ combine: THREE.MultiplyOperation,
|
|
|
+ transparent: true,
|
|
|
+ opacity: .9,
|
|
|
+ });
|
|
|
+ const material1 = new THREE.MeshLambertMaterial({ color: '#0088e6', transparent: true, opacity: 0.8 })
|
|
|
+ const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings)
|
|
|
+ const mesh = new THREE.Mesh(geometry, [material, material1])
|
|
|
+ mesh.rotateX(-Math.PI/2)
|
|
|
+ mesh.position.set(0,1.5,-3)
|
|
|
+ meshArrs.add(mesh)
|
|
|
+ meshs.push(mesh)
|
|
|
+ mesh.on('click', () => {
|
|
|
+ if (properties.name !== activeArea.value) {
|
|
|
+ activeArea.value = properties.name
|
|
|
+ ctx.emit('selectArea', properties.name)
|
|
|
+ } else {
|
|
|
+ if (activeArea.value === "") {
|
|
|
+ activeArea.value = properties.name
|
|
|
+ ctx.emit('selectArea', properties.name)
|
|
|
+ } else {
|
|
|
+ activeArea.value = ""
|
|
|
+ ctx.emit('selectArea', "重庆市")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+ mesh.on('mouseover', () => {
|
|
|
+ info.element.innerHTML = props.formatter(mapData.value[properties.name])
|
|
|
+ info.visible = true
|
|
|
+ })
|
|
|
+ mesh.on('mouseout', () => {
|
|
|
+ info.visible = false
|
|
|
+ })
|
|
|
+ cacheMap.set(properties.name, {
|
|
|
+ mesh,
|
|
|
+ name,
|
|
|
+ info
|
|
|
+ })
|
|
|
+
|
|
|
+ //Line2
|
|
|
+ linGeometry.setPositions( positions );
|
|
|
+ linGeometry.setColors( colors );
|
|
|
+ const line = new Line2( linGeometry, matLine );
|
|
|
+ const line2 = new Line2( linGeometry, matLine2 );
|
|
|
+ line.computeLineDistances();
|
|
|
+ line.rotateX(-Math.PI/2)
|
|
|
+ line2.rotateX(-Math.PI/2)
|
|
|
+ line.position.set(0,0.1,-3)
|
|
|
+ line2.position.set(0,-3.5,-3)
|
|
|
+ line2.computeLineDistances();
|
|
|
+ line.scale.set( 1, 1, 1 );
|
|
|
+ province.add(line)
|
|
|
+ province.add(line2)
|
|
|
+ })
|
|
|
+ })
|
|
|
+ map.add(province)
|
|
|
+ map.add(meshArrs)
|
|
|
+ })
|
|
|
+ scene.add(map)
|
|
|
+ }
|
|
|
+
|
|
|
+ function initLightPoint(properties,projection){
|
|
|
+ let lightCenter = properties.centroid || properties.center;
|
|
|
+ let areaName =properties.name;
|
|
|
+ // projection用来把经纬度转换成坐标
|
|
|
+ const [x, y]=projection(lightCenter)
|
|
|
+ //这里创建坐标
|
|
|
+ return [createTextPoint(x,y,areaName) , createInfoWindow(x,y, areaName) ];
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param {*} x
|
|
|
+ * @param {*} z
|
|
|
+ * @param {*} areaName 地区名称
|
|
|
+ */
|
|
|
+ function createTextPoint(x,z,areaName){
|
|
|
+ let tag = document.createElement('div')
|
|
|
+ tag.innerHTML = areaName
|
|
|
+ // tag.className = className
|
|
|
+ tag.style.pointerEvents = 'none'
|
|
|
+ // tag.style.visibility = 'hidden'
|
|
|
+ tag.style.position = 'absolute'
|
|
|
+ tag.style.color = '#fff'
|
|
|
+ let label = new CSS2DObject(tag)
|
|
|
+ label.element.innerHTML = areaName
|
|
|
+ label.element.style.visibility = 'visible'
|
|
|
+ label.position.set(x,5.01,z)
|
|
|
+ label.position.z-=3
|
|
|
+ scene.add(label)
|
|
|
+ return label;
|
|
|
+ }
|
|
|
+
|
|
|
+ function createInfoWindow(x, z, areaName) {
|
|
|
+ let tag = document.createElement('div')
|
|
|
+ tag.innerHTML = areaName
|
|
|
+ tag.style.pointerEvents = 'none'
|
|
|
+ tag.style.position = 'absolute'
|
|
|
+ tag.style.border = '1px solid rgba(61, 115, 255, 0.72)'
|
|
|
+ tag.style.borderRadius = '4px'
|
|
|
+ tag.style.backgroundColor = '#000000'
|
|
|
+ tag.style.color = '#fff'
|
|
|
+ let label = new CSS2DObject(tag)
|
|
|
+ label.element.innerHTML = ""
|
|
|
+ label.element.style.visibility = 'visible'
|
|
|
+ label.position.set(x,20,z)
|
|
|
+ label.position.z-=3
|
|
|
+ label.visible = false
|
|
|
+ scene.add(label)
|
|
|
+ return label;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ return {
|
|
|
+ mapTestBox
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<template >
|
|
|
+ <div ref="mapTestBox" class="map-chart-3d"></div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped lang='less'>
|
|
|
+.map-chart-3d {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+</style>
|