Files
kengee-data-screen/src/views/AppThreeMap.vue

423 lines
15 KiB
Vue

<script>
export default {
name: "AppThreeMap",
label: "3D地图",
data() {
return {
geoMap: null,
layers: [],
font: null,
geoJson: null
}
},
computed: {
search: v => v.$marketBoard.search
},
methods: {
loadLib() {
const {$waitFor, THREE, $loadScript} = window
return $waitFor(THREE).then(() => Promise.all([
`http://10.0.97.209/presource/datascreen/js/three/js/controls/OrbitControls.js`,
`http://10.0.97.209/presource/datascreen/js/three/js/renderers/CSS3DRenderer.js`,
`http://10.0.97.209/presource/datascreen/js/three/js/loaders/FontLoader.js`,
`http://10.0.97.209/presource/datascreen/js/three/js/geometries/TextGeometry.js`,
'/presource/datascreen/js/turf.min.js'
].map(e => $loadScript('js', e))))
},
initMap() {
const {THREE, d3, TWEEN, turf} = window
const rootEl = this.$el
const root = this
const scale = 8
class GeoMap {
constructor() {
this.cameraPosition = {x: 40, y: 0, z: 80}; // 相机位置
this.scene = null; // 场景
this.camera = null; // 相机
this.renderer = null; // 渲染器
this.CSS3DRenderer = null; // css渲染器
this.controls = null; // 控制器
this.mapGroup = new THREE.Group(); // 组
this.mouse = new THREE.Vector2();
this.font = null;
this.tips = new THREE.Group()
this.loop = 0
this.markers = new THREE.Group();
this.center = turf.center(root.geoJson).geometry.coordinates
}
/**
* @desc 初始化
* */
init() {
this.setScene();
this.setCamera();
// this.setAxes();
this.setRenderer();
this.setControl();
this.setMapData(root.geoJson)
this.setHoverPanel()
this.animation();
this.bindMouseEvent()
}
/**
* @desc 动画循环
* */
animation() {
requestAnimationFrame(this.animation.bind(this));
this.loop = (this.loop * 100 + 2) % 100 * 0.01
if (this.markers) {
this.markers.children.forEach(e => {
if (e.data.bakeStockAmt <= 0) {
e.scale = {
x: Math.asin(this.loop) + 1,
y: Math.asin(this.loop) + 1,
z: Math.asin(this.loop) + 1
};
}
})
}
TWEEN.update();
this.controls.update();
this.renderer.render(this.scene, this.camera);
this.CSS3DRenderer.render(this.scene, this.camera);
}
/**
* @desc 绘制地图
* @params geojson
* */
setMapData(data) {
const that = this
const getLnglat = (arr, cb) => {
arr?.map(e => {
if (e.length === 2 && typeof e[0] === 'number' && typeof e[1] === 'number') {
cb(e)
} else {
getLnglat(e, cb)
}
})
}
let vector3json = [],
vector3border = []
const maxBoundaryPoints = turf.union(data)
getLnglat(maxBoundaryPoints.geometry.coordinates, p => {
const lnglat = that.lnglatToVector3(p);
const vector3 = new THREE.Vector3(lnglat[0], lnglat[1], lnglat[2]).multiplyScalar(1.2);
vector3border.push(vector3)
})
data.features.forEach(function (features, featuresIndex) {
const areaItems = features.geometry.coordinates;
features.properties.cp = that.lnglatToVector3(turf.center(features).geometry.coordinates);
vector3json[featuresIndex] = {
data: features.properties,
mercator: []
};
areaItems.forEach(function (item, areaIndex) {
vector3json[featuresIndex].mercator[areaIndex] = [];
getLnglat(item, cp => {
const lnglat = that.lnglatToVector3(cp);
const vector3 = new THREE.Vector3(lnglat[0], lnglat[1], lnglat[2]).multiplyScalar(1.2);
vector3json[featuresIndex].mercator[areaIndex].push(vector3)
})
})
});
this.drawMap(vector3json, vector3border)
}
/**
* @desc 绘制图形
* @param data
* */
drawMap(data, border) {
let that = this;
this.mapGroup.position.y = 0;
const extrudeSettings = {
depth: 0.1,
steps: 1,
bevelSegments: 0,
curveSegments: 1,
bevelEnabled: false,
};
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 256;
const ctx = canvas.getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
gradient.addColorStop(0, 'rgba(61,127,255,0.35)');
gradient.addColorStop(1, '#09E2F8');
// gradient.addColorStop(0, 'rgba(61,127,255,0.01)'); // 结束颜色
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
const blockMaterial = new THREE.MeshBasicMaterial({
map: new THREE.CanvasTexture(canvas),
transparent: true, wireframe: false
});
const lineMaterial = new THREE.LineBasicMaterial({color: '#97CAE6'});
data.forEach(function (areaData) {
let areaGroup = new THREE.Group();
areaGroup.name = 'area';
areaGroup._groupType = 'areaBlock';
areaData.mercator.forEach(function (areaItem) {
// Draw Line
let lineGeometry = new THREE.BufferGeometry().setFromPoints(areaItem);
let lineMesh = new THREE.Line(lineGeometry, lineMaterial);
lineMesh.position.z = 0.101;
areaGroup.add(lineMesh);
});
const {name, cp} = areaData.data
that.setTips(name, cp[0], cp[1])
// areaGroup.add(that.tipsSprite(areaData));
that.mapGroup.add(areaGroup);
});
that.mapGroup.add(that.tips)
const shape = new THREE.Shape(border);
const areaGeometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
const borderMaterial = new THREE.MeshBasicMaterial({color: "#1B4C92", transparent: true, opacity: 0.8});
const mesh = new THREE.Mesh(areaGeometry, [blockMaterial, borderMaterial]);
that.mapGroup.add(mesh)
}
transLayer(item = {}) {
let {bakeStockAmt, longitude, latitude} = item
longitude = Number(longitude || 0).toFixed(6);
latitude = Number(latitude || 0).toFixed(6);
const markerGeometry = new THREE.CircleGeometry(0.015, 32);
const markerMaterial = new THREE.MeshBasicMaterial({
wireframe: false,
color: bakeStockAmt > 0 ? "#66FFFF" : "#FFD15C",
});
const marker = new THREE.Mesh(markerGeometry, markerMaterial);
const lnglat = this.lnglatToVector3([longitude, latitude]);
let v3 = new THREE.Vector3(lnglat[0], lnglat[1], lnglat[2]).multiplyScalar(1.2);
marker.data = item
marker.position.set(v3.x, v3.y, 0.11)
return marker
}
addMarkers() {
this.markers.clear()
root.layers.map(layer => {
const marker = this.transLayer(layer)
this.markers.add(marker)
})
}
lnglatToVector3(lnglat = []) {
if (!this.projection) {
this.projection = d3.geoMercator().center(this.center).scale(100).translate([0, 0]);
}
const [x, y] = this.projection([lnglat[0], lnglat[1]])
const z = 0;
return [y, x, z]
}
/**
* @desc 创建场景
* */
setScene() {
this.scene = new THREE.Scene();
this.mapGroup.add(this.markers)
this.mapGroup.scale.set(scale, scale, scale)
this.mapGroup.position.set(0, 0, 0)
this.scene.add(this.mapGroup);
}
/**
* @desc 创建相机
* */
setCamera() {
this.camera = new THREE.PerspectiveCamera(10, rootEl.offsetWidth / rootEl.offsetHeight, 1, 2000);
this.camera.up.x = 0;
this.camera.up.y = 0;
this.camera.up.z = 1;
this.camera.lookAt(0, 0, 0);
this.scene.add(this.camera);
}
/**
* @desc 创建渲染器
* */
setRenderer() {
this.renderer = new THREE.WebGLRenderer({antialias: true});
this.renderer.setPixelRatio(window.devicePixelRatio);
this.renderer.sortObjects = true; // 渲染顺序
this.renderer.setClearColor(0xffffff, 0);
this.renderer.setSize(rootEl.offsetWidth, rootEl.offsetHeight);
rootEl.appendChild(this.renderer.domElement);
this.CSS3DRenderer = new THREE.CSS3DRenderer();
this.CSS3DRenderer.setSize(rootEl.offsetWidth, rootEl.offsetHeight);
this.CSS3DRenderer.domElement.style.position = 'absolute';
this.CSS3DRenderer.domElement.style.top = '0';
this.CSS3DRenderer.domElement.style.left = '0';
rootEl.appendChild(this.CSS3DRenderer.domElement);
function onWindowResize() {
this.camera.aspect = rootEl.offsetWidth / rootEl.offsetHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(rootEl.offsetWidth, rootEl.offsetHeight);
this.CSS3DRenderer.setSize(rootEl.offsetWidth, rootEl.offsetHeight);
}
rootEl.addEventListener('resize', onWindowResize.bind(this), false);
}
/**
* @desc 创建控制器
* */
setControl() {
this.controls = new THREE.OrbitControls(this.camera, rootEl);
this.controls.enableRotate = false
this.camera.position.set(this.cameraPosition.x, this.cameraPosition.y, this.cameraPosition.z);
}
/**
* @desc 创建一个xyz坐标轴
* */
setAxes() {
const axes = new THREE.AxesHelper(100);
this.scene.add(axes);
}
setTips(text, x, y) {
const textGeometry = new THREE.TextGeometry(text, {
font: root.font,
size: 0.04,
height: 0.02
});
textGeometry.center()
textGeometry.rotateZ(Math.PI / 2)
textGeometry.rotateY(Math.PI / 6)
textGeometry.translate(x, y, 0.15)
const textMaterial = new THREE.MeshBasicMaterial({color: 0xffffff, transparent: true, opacity: 0.8});
const textMesh = new THREE.Mesh(textGeometry, textMaterial);
this.tips.add(textMesh)
}
setHoverPanel() {
this.hoverPanelElement = document.createElement('div')
Object.entries({
background: 'linear-gradient( 90deg, rgba(9,63,107,0.79) 0%, rgba(13,58,99,0.03) 100%)',
borderRadius: '2px',
color: '#fff',
fontSize: '10px',
lineHeight: '14px',
position: 'absolute',
top: 0,
padding: '8px',
opacity: 0,
}).forEach(([key, value]) => {
this.hoverPanelElement.style[key] = value
})
this.hoverPanel = new THREE.CSS3DObject(this.hoverPanelElement)
this.scene.add(this.hoverPanel)
}
bindMouseEvent() {
const raycaster = new THREE.Raycaster();
const targets = this.markers?.children || []
const onPointerMove = (event) => {
const {offsetWidth: width, offsetHeight: height} = rootEl, {left, top} = rootEl.getBoundingClientRect();
this.mouse.x = ((event.clientX - left) / width) * 2 - 1; //标准设备横坐标
this.mouse.y = -((event.clientY - top) / height) * 2 + 1; //标准设备纵坐标
raycaster.setFromCamera(this.mouse, this.camera);
const intersects = raycaster.intersectObjects(targets)
intersects.forEach(e => {
if (e.object?.data) {
this.showHoverPanel(e.object)
}
})
if (intersects?.length <= 0) {
this.hoverPanelElement.style.opacity = '0'
}
}
const onClick = () => {
// const {x, y} = this.mouse
// const standardVector = new THREE.Vector3(x, y, 0.5); //标准设备坐标
// //标准设备坐标转世界坐标
// const worldVector = standardVector.unproject(this.camera);
// const ray = worldVector.sub(this.camera.position).normalize();
// 创建一个射线投射器
raycaster.setFromCamera(this.mouse, this.camera);
const intersects = raycaster.intersectObjects(targets)
intersects.forEach(e => {
if (e.object?.data) {
const marker = e.object?.data
root.$storeBoard.search.storeCode = marker.storeCode
root.$marketBoard.screenId = 'a90522ef-869b-40ea-8542-d1fc9674a1e8'
}
})
}
rootEl.addEventListener('pointermove', onPointerMove);
rootEl.addEventListener('click', onClick);
}
showHoverPanel({data: marker, position} = {}) {
this.hoverPanelElement.style.opacity = '1'
this.hoverPanelElement.innerHTML = [marker.storeName, `现烤库存金额:${marker.bakeStockAmt}`, `现烤销售机会:${marker.preSaleNum}`].join('<br/>')
this.hoverPanel.scale.set(...Array(3).fill(0.04))
this.hoverPanel.rotation.z = Math.PI / 2
this.hoverPanel.rotation.y = Math.PI / 6
this.hoverPanel.position.set(position.x * scale, position.y * scale, position.z * scale)
}
}
return new GeoMap()
},
getData() {
const {$http, $waitFor} = window
const {groupCodeList, currentDate, hourNum} = this.search
return $waitFor($http).then(() => Promise.all([
$http.post("/data-boot/la/screen/marketBoard/storeReport", {
groupCodeList, currentDate, hourNum
}).then(res => {
if (res?.data) {
return this.layers = res.data || []
}
}),
axios.get('http://10.0.97.209/blade-visual/map/data?id=1456').then(res => {
if (res?.data) {
return this.geoJson = res.data
}
})
]))
}
},
watch: {
search: {
deep: true, handler() {
this.getData().then(() => this.geoMap.addMarkers())
}
}
},
mounted() {
this.loadLib().then(() => new Promise(resolve => {
const loader = new THREE.FontLoader();
loader.load("/presource/datascreen/js/three/fonts/HarmonyOS Sans SC_Regular.json", font => {
this.font = font
resolve()
})
})).then(() => {
return !this.geoJson && this.getData()
}).then(() => {
this.geoMap = this.initMap();
this.geoMap.init();
this.geoMap.addMarkers()
})
}
}
</script>
<template>
<section class="AppThreeMap"/>
</template>
<style>
.AppThreeMap {
width: 100%;
height: 100%;
position: relative;
}
</style>