import * as KMap from "@/utils/ol-map/KMap"; import { Vector as VectorSource, Cluster } from "ol/source.js"; import { Feature } from "ol"; import { WKT } from "ol/format"; import { Circle, Fill, Stroke, Style, Text, Icon, RegularShape } from "ol/style.js"; import Photo from "ol-ext/style/Photo"; import { newPoint, newPolymerFeature } from "@/utils/map.js"; import pointBgImg from "@/assets/images/map/point_bg.png"; class DistributionLayer { constructor(kmap) { let that = this; this.kmap = kmap let vectorStyle = new KMap.VectorStyle() this.distributionLayer = new KMap.VectorLayer("distributionLayer", 99, { source: new VectorSource({}), style: function (f) { let style2 = vectorStyle.getPolygonStyle(f.get("color")+'4d', f.get("color"), 2) return [style2] } }); this.kmap.addLayer(this.distributionLayer.layer) // 创建聚合数据源 this.clusterSource = new Cluster({ distance: 80, // 聚合距离(像素) minDistance: 50, // 最小聚合距离 }); // 提取单个点样式函数 const getPointStyle = (feature, isCluster = false, clusterSize = 1) => { const label = feature.get("label") || ""; const baseColor = feature.get("color") || "#2199F8"; const farmName = feature.get("farmName") || ""; const imgName = feature.get("imgName") || ""; const speciesIcon = feature.get("speciesIcon"); // 方块填充色背景(在白色描边 PNG 底图下方,参考 UI) const squareBgStyle = new Style({ image: new RegularShape({ points: 4, radius: 34, // 控制方块大小 angle: Math.PI / 4, // 旋转 45°,让正方形对齐 fill: new Fill({ color: baseColor, }), imgSize: [34, 34], // 向上平移一点,让方块主要位于指针上方 displacement: [0, 4], }), }); const iconAndTextStyle = new Style({ image: new Icon({ src: pointBgImg, scale: 0.45, }), // 文字 + 背景(参考 UI:白色圆角矩形 + 彩色文字) text: new Text({ text: label, font: "normal 14px sans-serif", offsetY: -46, // 文字整体上移,避免和图片重叠 textAlign: "center", fill: new Fill({ color: baseColor, }), backgroundFill: new Fill({ color: "rgba(255,255,255,1)", }), backgroundStroke: new Stroke({ color: baseColor, width: 1, }), padding: [2, 14, 2, 14], }), }); // 聚合点数量徽章(红色圆形,白色文字,图片右上角) // 图片 scale 0.45,假设原始图片约 100x100,实际显示约 45x45 // 右上角位置:offsetX 约 22-25,offsetY 约 -22-25(相对于图片中心) const badgeStyle = isCluster && clusterSize > 1 ? new Style({ image: new Circle({ radius: 10, fill: new Fill({ color: "#FF0000", // 红色 }), // 定位到图片右上角 displacement: [22, 25], // 相对于图片中心,向右上偏移 }), text: new Text({ text: clusterSize.toString(), font: "bold 11px sans-serif", fill: new Fill({ color: "#FFFFFF", // 白色文字 }), // 文字位置与圆形位置一致 offsetX: 22, offsetY: -25, textAlign: "center", textBaseline: "middle", }), }) : null; // 农场名称(显示在点位下方,白色文字) // 聚合点显示第一个点的农场名称 const farmNameStyle = farmName ? new Style({ text: new Text({ text: farmName, font: "normal 18px sans-serif", offsetY: 45, // 向下偏移,位于点位下方 textAlign: "center", fill: new Fill({ color: "#FFFFFF", }), stroke: new Stroke({ color: "rgba(0,0,0,0.6)", width: 2, }), }), }) : null; const typeImgStyle = new Style({ image: new Icon({ // src: imgUrl, src: speciesIcon, scale: 0.8, displacement: [0, 4], }), }); // 先画纯色方块,再画白色描边 PNG 和文字,然后画徽章,最后画农场名称 const styles = [squareBgStyle, iconAndTextStyle]; if (speciesIcon) styles.push(typeImgStyle); if (badgeStyle) styles.push(badgeStyle); if (farmNameStyle) styles.push(farmNameStyle); return styles; }; this.distributionPointLayer = new KMap.VectorLayer("distributionPointLayer", 99, { source: this.clusterSource, style: (f) => { // 判断是否为聚合点 const features = f.get('features'); if (features && features.length > 1) { // 聚合点:使用第一个点的样式,但添加数量标识 const firstFeature = features[0]; return getPointStyle(firstFeature, true, features.length); } // 单个点样式(原有逻辑) const singleFeature = features && features.length === 1 ? features[0] : f; return getPointStyle(singleFeature, false, 1); }, }); this.kmap.addLayer(this.distributionPointLayer.layer) // 设施图层:只显示图标(冷链冷库 / 加工厂等),不带边框和文字 this.facilityPointLayer = new KMap.VectorLayer("facilityPointLayer", 100, { source: new VectorSource({}), style: function (f) { const imgName = f.get("imgName") || ""; return new Style({ image: new Icon({ src: imgName, imgSize: [45, 45], width: 45, height: 45, scale: 0.9, }), }); }, }); this.kmap.addLayer(this.facilityPointLayer.layer) } /** * 清空当前图层上的所有数据 */ clear() { if (this.distributionLayer && this.distributionLayer.source) { this.distributionLayer.source.clear(); } if (this.clusterSource && this.clusterSource.source) { this.clusterSource.source.clear(); } if (this.facilityPointLayer && this.facilityPointLayer.source) { this.facilityPointLayer.source.clear(); } } initData(data, field = 'speciesName') { // 每次加载前先清空旧数据(多用于"作物分布 / 物候期分布 / 农场分布") this.clear(); if(!data || data.length === 0) return; // 创建临时 VectorSource 用于存储点数据 const pointSource = new VectorSource({}); for (let item of data) { // 面数据(区域多边形) if (item.geom) { item.color = item.speciesColor || "#2199F8"; this.distributionLayer.source.addFeature(newPolymerFeature(item)); } if (item.centerPoint) { item.color = item.speciesColor || "#2199F8"; item.wkt = item.centerPoint; item.label = item[field] || ""; pointSource.addFeature(newPoint(item)); } } // 将点数据源设置到聚合源 if (pointSource.getFeatures().length > 0) { this.clusterSource.setSource(pointSource); } // const extent = this.distributionLayer.source.getExtent(); // if (extent && !isNaN(extent[0])) { // this.kmap.map.getView().fit(extent, { // padding: [280, 400, 200, 150], // duration: 500, // }); // } // 所有点位添加完成后,地图范围自适应到包含所有点 if (pointSource.getFeatures().length > 0) { setTimeout(() => { const pointExtent = this.clusterSource.source.getExtent(); if (pointExtent && !isNaN(pointExtent[0])) { this.kmap.map.getView().fit(pointExtent, { padding: [280, 400, 200, 150], duration: 500, maxZoom: 12, }); } }, 100); } } /** * 设施数据加载(冷链冷库 / 加工厂) * 只操作 facilityPointLayer,不影响作物分布图层 */ initFacilityData(list = []) { if (!this.facilityPointLayer || !this.facilityPointLayer.source) return; this.facilityPointLayer.source.clear(); const extentSource = new VectorSource({}); for (let item of list) { if (!item.wktArr) continue; for (let wkt of item.wktArr) { const f = new Feature({ geometry: new WKT().readGeometry(wkt), }); f.set("imgName", item.imgName); this.facilityPointLayer.source.addFeature(f); extentSource.addFeature(f); } } const extent = extentSource.getExtent(); if (extent && !isNaN(extent[0])) { this.kmap.map.getView().fit(extent, { padding: [280, 400, 200, 150], duration: 500, }); } } } export default DistributionLayer;