|
@@ -1,5 +1,5 @@
|
|
|
import * as KMap from "@/utils/ol-map/KMap";
|
|
import * as KMap from "@/utils/ol-map/KMap";
|
|
|
-import { Vector as VectorSource } from "ol/source.js";
|
|
|
|
|
|
|
+import { Vector as VectorSource, Cluster } from "ol/source.js";
|
|
|
import { Feature } from "ol";
|
|
import { Feature } from "ol";
|
|
|
import { WKT } from "ol/format";
|
|
import { WKT } from "ol/format";
|
|
|
import { Circle, Fill, Stroke, Style, Text, Icon, RegularShape } from "ol/style.js";
|
|
import { Circle, Fill, Stroke, Style, Text, Icon, RegularShape } from "ol/style.js";
|
|
@@ -22,87 +22,137 @@ class DistributionLayer {
|
|
|
});
|
|
});
|
|
|
this.kmap.addLayer(this.distributionLayer.layer)
|
|
this.kmap.addLayer(this.distributionLayer.layer)
|
|
|
|
|
|
|
|
- this.distributionPointLayer = new KMap.VectorLayer("distributionPointLayer", 99, {
|
|
|
|
|
- source: new VectorSource({}),
|
|
|
|
|
- style: function (f) {
|
|
|
|
|
- const label = f.get("label") || "";
|
|
|
|
|
- const baseColor = f.get("color") || "#2199F8";
|
|
|
|
|
- const farmName = f.get("farmName") || "";
|
|
|
|
|
- const imgName = f.get("imgName") || "";
|
|
|
|
|
- const speciesIcon = f.get("speciesIcon");
|
|
|
|
|
-
|
|
|
|
|
- // 方块填充色背景(在白色描边 PNG 底图下方,参考 UI)
|
|
|
|
|
- const squareBgStyle = new Style({
|
|
|
|
|
- image: new RegularShape({
|
|
|
|
|
- points: 4,
|
|
|
|
|
- radius: 34, // 控制方块大小
|
|
|
|
|
- angle: Math.PI / 4, // 旋转 45°,让正方形对齐
|
|
|
|
|
|
|
+ // 创建聚合数据源
|
|
|
|
|
+ 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({
|
|
fill: new Fill({
|
|
|
- color: baseColor,
|
|
|
|
|
|
|
+ color: "#FF0000", // 红色
|
|
|
}),
|
|
}),
|
|
|
- imgSize: [34, 34],
|
|
|
|
|
- // 向上平移一点,让方块主要位于指针上方
|
|
|
|
|
- displacement: [0, 4],
|
|
|
|
|
|
|
+ // 定位到图片右上角
|
|
|
|
|
+ displacement: [22, 25], // 相对于图片中心,向右上偏移
|
|
|
}),
|
|
}),
|
|
|
- });
|
|
|
|
|
-
|
|
|
|
|
- const iconAndTextStyle = new Style({
|
|
|
|
|
- image: new Icon({
|
|
|
|
|
- src: pointBgImg,
|
|
|
|
|
- scale: 0.45,
|
|
|
|
|
|
|
+ text: new Text({
|
|
|
|
|
+ text: clusterSize.toString(),
|
|
|
|
|
+ font: "bold 11px sans-serif",
|
|
|
|
|
+ fill: new Fill({
|
|
|
|
|
+ color: "#FFFFFF", // 白色文字
|
|
|
|
|
+ }),
|
|
|
|
|
+ // 文字位置与圆形位置一致
|
|
|
|
|
+ offsetX: 22,
|
|
|
|
|
+ offsetY: -25,
|
|
|
|
|
+ textAlign: "center",
|
|
|
|
|
+ textBaseline: "middle",
|
|
|
}),
|
|
}),
|
|
|
- // 文字 + 背景(参考 UI:白色圆角矩形 + 彩色文字)
|
|
|
|
|
|
|
+ })
|
|
|
|
|
+ : null;
|
|
|
|
|
+
|
|
|
|
|
+ // 农场名称(显示在点位下方,白色文字)
|
|
|
|
|
+ // 聚合点显示第一个点的农场名称
|
|
|
|
|
+ const farmNameStyle = farmName
|
|
|
|
|
+ ? new Style({
|
|
|
text: new Text({
|
|
text: new Text({
|
|
|
- text: label,
|
|
|
|
|
- font: "normal 14px sans-serif",
|
|
|
|
|
- offsetY: -46, // 文字整体上移,避免和图片重叠
|
|
|
|
|
|
|
+ text: farmName,
|
|
|
|
|
+ font: "normal 18px sans-serif",
|
|
|
|
|
+ offsetY: 45, // 向下偏移,位于点位下方
|
|
|
textAlign: "center",
|
|
textAlign: "center",
|
|
|
fill: new Fill({
|
|
fill: new Fill({
|
|
|
- color: baseColor,
|
|
|
|
|
- }),
|
|
|
|
|
- backgroundFill: new Fill({
|
|
|
|
|
- color: "rgba(255,255,255,1)",
|
|
|
|
|
|
|
+ color: "#FFFFFF",
|
|
|
}),
|
|
}),
|
|
|
- backgroundStroke: new Stroke({
|
|
|
|
|
- color: baseColor,
|
|
|
|
|
- width: 1,
|
|
|
|
|
|
|
+ stroke: new Stroke({
|
|
|
|
|
+ color: "rgba(0,0,0,0.6)",
|
|
|
|
|
+ width: 2,
|
|
|
}),
|
|
}),
|
|
|
- padding: [2, 14, 2, 14],
|
|
|
|
|
}),
|
|
}),
|
|
|
- });
|
|
|
|
|
|
|
+ })
|
|
|
|
|
+ : null;
|
|
|
|
|
+ const typeImgStyle = new Style({
|
|
|
|
|
+ image: new Icon({
|
|
|
|
|
+ // src: imgUrl,
|
|
|
|
|
+ src: speciesIcon,
|
|
|
|
|
+ scale: 0.8,
|
|
|
|
|
+ displacement: [0, 4],
|
|
|
|
|
+ }),
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
- // 农场名称(显示在点位下方,白色文字)
|
|
|
|
|
- const farmNameStyle = farmName
|
|
|
|
|
- ? new Style({
|
|
|
|
|
- text: new Text({
|
|
|
|
|
- text: farmName,
|
|
|
|
|
- font: "normal 19px 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;
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
- // 先画纯色方块,再画白色描边 PNG 和文字,最后画农场名称
|
|
|
|
|
- const styles = [squareBgStyle, iconAndTextStyle];
|
|
|
|
|
- if (speciesIcon) styles.push(typeImgStyle);
|
|
|
|
|
- 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.kmap.addLayer(this.distributionPointLayer.layer)
|
|
@@ -133,8 +183,8 @@ class DistributionLayer {
|
|
|
if (this.distributionLayer && this.distributionLayer.source) {
|
|
if (this.distributionLayer && this.distributionLayer.source) {
|
|
|
this.distributionLayer.source.clear();
|
|
this.distributionLayer.source.clear();
|
|
|
}
|
|
}
|
|
|
- if (this.distributionPointLayer && this.distributionPointLayer.source) {
|
|
|
|
|
- this.distributionPointLayer.source.clear();
|
|
|
|
|
|
|
+ if (this.clusterSource && this.clusterSource.source) {
|
|
|
|
|
+ this.clusterSource.source.clear();
|
|
|
}
|
|
}
|
|
|
if (this.facilityPointLayer && this.facilityPointLayer.source) {
|
|
if (this.facilityPointLayer && this.facilityPointLayer.source) {
|
|
|
this.facilityPointLayer.source.clear();
|
|
this.facilityPointLayer.source.clear();
|
|
@@ -142,10 +192,13 @@ class DistributionLayer {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
initData(data, field = 'speciesName') {
|
|
initData(data, field = 'speciesName') {
|
|
|
- // 每次加载前先清空旧数据(多用于“作物分布 / 物候期分布 / 农场分布”)
|
|
|
|
|
|
|
+ // 每次加载前先清空旧数据(多用于"作物分布 / 物候期分布 / 农场分布")
|
|
|
this.clear();
|
|
this.clear();
|
|
|
if(!data || data.length === 0) return;
|
|
if(!data || data.length === 0) return;
|
|
|
|
|
|
|
|
|
|
+ // 创建临时 VectorSource 用于存储点数据
|
|
|
|
|
+ const pointSource = new VectorSource({});
|
|
|
|
|
+
|
|
|
for (let item of data) {
|
|
for (let item of data) {
|
|
|
// 面数据(区域多边形)
|
|
// 面数据(区域多边形)
|
|
|
if (item.geom) {
|
|
if (item.geom) {
|
|
@@ -156,9 +209,14 @@ class DistributionLayer {
|
|
|
item.color = item.speciesColor || "#2199F8";
|
|
item.color = item.speciesColor || "#2199F8";
|
|
|
item.wkt = item.centerPoint;
|
|
item.wkt = item.centerPoint;
|
|
|
item.label = item[field] || "";
|
|
item.label = item[field] || "";
|
|
|
- this.distributionPointLayer.source.addFeature(newPoint(item));
|
|
|
|
|
|
|
+ pointSource.addFeature(newPoint(item));
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // 将点数据源设置到聚合源
|
|
|
|
|
+ if (pointSource.getFeatures().length > 0) {
|
|
|
|
|
+ this.clusterSource.setSource(pointSource);
|
|
|
|
|
+ }
|
|
|
// const extent = this.distributionLayer.source.getExtent();
|
|
// const extent = this.distributionLayer.source.getExtent();
|
|
|
// if (extent && !isNaN(extent[0])) {
|
|
// if (extent && !isNaN(extent[0])) {
|
|
|
// this.kmap.map.getView().fit(extent, {
|
|
// this.kmap.map.getView().fit(extent, {
|
|
@@ -167,13 +225,17 @@ class DistributionLayer {
|
|
|
// });
|
|
// });
|
|
|
// }
|
|
// }
|
|
|
|
|
|
|
|
- // 所有点位添加完成后,地图范围自适应到包含 distributionPointLayer 的所有点
|
|
|
|
|
- const pointExtent = this.distributionPointLayer.source.getExtent();
|
|
|
|
|
- if (pointExtent && !isNaN(pointExtent[0])) {
|
|
|
|
|
- this.kmap.map.getView().fit(pointExtent, {
|
|
|
|
|
- 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,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 100);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|