423 lines
15 KiB
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>
|