distributionLayer.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. import * as KMap from "@/utils/ol-map/KMap";
  2. import { Vector as VectorSource, Cluster } from "ol/source.js";
  3. import { Feature } from "ol";
  4. import { WKT } from "ol/format";
  5. import { Circle, Fill, Stroke, Style, Text, Icon, RegularShape } from "ol/style.js";
  6. import Photo from "ol-ext/style/Photo";
  7. import { newPoint, newPolymerFeature } from "@/utils/map.js";
  8. import pointBgImg from "@/assets/images/map/point_bg.png";
  9. class DistributionLayer {
  10. constructor(kmap) {
  11. let that = this;
  12. this.kmap = kmap
  13. let vectorStyle = new KMap.VectorStyle()
  14. this.distributionLayer = new KMap.VectorLayer("distributionLayer", 99, {
  15. source: new VectorSource({}),
  16. style: function (f) {
  17. let style2 = vectorStyle.getPolygonStyle(f.get("color")+'4d', f.get("color"), 2)
  18. return [style2]
  19. }
  20. });
  21. this.kmap.addLayer(this.distributionLayer.layer)
  22. // 创建聚合数据源
  23. this.clusterSource = new Cluster({
  24. distance: 80, // 聚合距离(像素)
  25. minDistance: 50, // 最小聚合距离
  26. });
  27. // 提取单个点样式函数
  28. const getPointStyle = (feature, isCluster = false, clusterSize = 1) => {
  29. const label = feature.get("label") || "";
  30. const baseColor = feature.get("color") || "#2199F8";
  31. const farmName = feature.get("farmName") || "";
  32. const imgName = feature.get("imgName") || "";
  33. const speciesIcon = feature.get("speciesIcon");
  34. // 方块填充色背景(在白色描边 PNG 底图下方,参考 UI)
  35. const squareBgStyle = new Style({
  36. image: new RegularShape({
  37. points: 4,
  38. radius: 34, // 控制方块大小
  39. angle: Math.PI / 4, // 旋转 45°,让正方形对齐
  40. fill: new Fill({
  41. color: baseColor,
  42. }),
  43. imgSize: [34, 34],
  44. // 向上平移一点,让方块主要位于指针上方
  45. displacement: [0, 4],
  46. }),
  47. });
  48. const iconAndTextStyle = new Style({
  49. image: new Icon({
  50. src: pointBgImg,
  51. scale: 0.45,
  52. }),
  53. // 文字 + 背景(参考 UI:白色圆角矩形 + 彩色文字)
  54. text: new Text({
  55. text: label,
  56. font: "normal 14px sans-serif",
  57. offsetY: -46, // 文字整体上移,避免和图片重叠
  58. textAlign: "center",
  59. fill: new Fill({
  60. color: baseColor,
  61. }),
  62. backgroundFill: new Fill({
  63. color: "rgba(255,255,255,1)",
  64. }),
  65. backgroundStroke: new Stroke({
  66. color: baseColor,
  67. width: 1,
  68. }),
  69. padding: [2, 14, 2, 14],
  70. }),
  71. });
  72. // 聚合点数量徽章(红色圆形,白色文字,图片右上角)
  73. // 图片 scale 0.45,假设原始图片约 100x100,实际显示约 45x45
  74. // 右上角位置:offsetX 约 22-25,offsetY 约 -22-25(相对于图片中心)
  75. const badgeStyle = isCluster && clusterSize > 1
  76. ? new Style({
  77. image: new Circle({
  78. radius: 10,
  79. fill: new Fill({
  80. color: "#FF0000", // 红色
  81. }),
  82. // 定位到图片右上角
  83. displacement: [22, 25], // 相对于图片中心,向右上偏移
  84. }),
  85. text: new Text({
  86. text: clusterSize.toString(),
  87. font: "bold 11px sans-serif",
  88. fill: new Fill({
  89. color: "#FFFFFF", // 白色文字
  90. }),
  91. // 文字位置与圆形位置一致
  92. offsetX: 22,
  93. offsetY: -25,
  94. textAlign: "center",
  95. textBaseline: "middle",
  96. }),
  97. })
  98. : null;
  99. // 农场名称(显示在点位下方,白色文字)
  100. // 聚合点显示第一个点的农场名称
  101. const farmNameStyle = farmName
  102. ? new Style({
  103. text: new Text({
  104. text: farmName,
  105. font: "normal 18px sans-serif",
  106. offsetY: 45, // 向下偏移,位于点位下方
  107. textAlign: "center",
  108. fill: new Fill({
  109. color: "#FFFFFF",
  110. }),
  111. stroke: new Stroke({
  112. color: "rgba(0,0,0,0.6)",
  113. width: 2,
  114. }),
  115. }),
  116. })
  117. : null;
  118. const typeImgStyle = new Style({
  119. image: new Icon({
  120. // src: imgUrl,
  121. src: speciesIcon,
  122. scale: 0.8,
  123. displacement: [0, 4],
  124. }),
  125. });
  126. // 先画纯色方块,再画白色描边 PNG 和文字,然后画徽章,最后画农场名称
  127. const styles = [squareBgStyle, iconAndTextStyle];
  128. if (speciesIcon) styles.push(typeImgStyle);
  129. if (badgeStyle) styles.push(badgeStyle);
  130. if (farmNameStyle) styles.push(farmNameStyle);
  131. return styles;
  132. };
  133. this.distributionPointLayer = new KMap.VectorLayer("distributionPointLayer", 99, {
  134. source: this.clusterSource,
  135. style: (f) => {
  136. // 判断是否为聚合点
  137. const features = f.get('features');
  138. if (features && features.length > 1) {
  139. // 聚合点:使用第一个点的样式,但添加数量标识
  140. const firstFeature = features[0];
  141. return getPointStyle(firstFeature, true, features.length);
  142. }
  143. // 单个点样式(原有逻辑)
  144. const singleFeature = features && features.length === 1 ? features[0] : f;
  145. return getPointStyle(singleFeature, false, 1);
  146. },
  147. });
  148. this.kmap.addLayer(this.distributionPointLayer.layer)
  149. // 设施图层:只显示图标(冷链冷库 / 加工厂等),不带边框和文字
  150. this.facilityPointLayer = new KMap.VectorLayer("facilityPointLayer", 100, {
  151. source: new VectorSource({}),
  152. style: function (f) {
  153. const imgName = f.get("imgName") || "";
  154. return new Style({
  155. image: new Icon({
  156. src: imgName,
  157. imgSize: [45, 45],
  158. width: 45,
  159. height: 45,
  160. scale: 0.9,
  161. }),
  162. });
  163. },
  164. });
  165. this.kmap.addLayer(this.facilityPointLayer.layer)
  166. }
  167. /**
  168. * 清空当前图层上的所有数据
  169. */
  170. clear() {
  171. if (this.distributionLayer && this.distributionLayer.source) {
  172. this.distributionLayer.source.clear();
  173. }
  174. if (this.clusterSource && this.clusterSource.source) {
  175. this.clusterSource.source.clear();
  176. }
  177. if (this.facilityPointLayer && this.facilityPointLayer.source) {
  178. this.facilityPointLayer.source.clear();
  179. }
  180. }
  181. initData(data, field = 'speciesName') {
  182. // 每次加载前先清空旧数据(多用于"作物分布 / 物候期分布 / 农场分布")
  183. this.clear();
  184. if(!data || data.length === 0) return;
  185. // 创建临时 VectorSource 用于存储点数据
  186. const pointSource = new VectorSource({});
  187. for (let item of data) {
  188. // 面数据(区域多边形)
  189. if (item.geom) {
  190. item.color = item.speciesColor || "#2199F8";
  191. this.distributionLayer.source.addFeature(newPolymerFeature(item));
  192. }
  193. if (item.centerPoint) {
  194. item.color = item.speciesColor || "#2199F8";
  195. item.wkt = item.centerPoint;
  196. item.label = item[field] || "";
  197. pointSource.addFeature(newPoint(item));
  198. }
  199. }
  200. // 将点数据源设置到聚合源
  201. if (pointSource.getFeatures().length > 0) {
  202. this.clusterSource.setSource(pointSource);
  203. }
  204. // const extent = this.distributionLayer.source.getExtent();
  205. // if (extent && !isNaN(extent[0])) {
  206. // this.kmap.map.getView().fit(extent, {
  207. // padding: [280, 400, 200, 150],
  208. // duration: 500,
  209. // });
  210. // }
  211. // 所有点位添加完成后,地图范围自适应到包含所有点
  212. if (pointSource.getFeatures().length > 0) {
  213. setTimeout(() => {
  214. const pointExtent = this.clusterSource.source.getExtent();
  215. if (pointExtent && !isNaN(pointExtent[0])) {
  216. this.kmap.map.getView().fit(pointExtent, {
  217. padding: [280, 400, 200, 150],
  218. duration: 500,
  219. maxZoom: 12,
  220. });
  221. }
  222. }, 100);
  223. }
  224. }
  225. /**
  226. * 设施数据加载(冷链冷库 / 加工厂)
  227. * 只操作 facilityPointLayer,不影响作物分布图层
  228. */
  229. initFacilityData(list = []) {
  230. if (!this.facilityPointLayer || !this.facilityPointLayer.source) return;
  231. this.facilityPointLayer.source.clear();
  232. const extentSource = new VectorSource({});
  233. for (let item of list) {
  234. if (!item.wktArr) continue;
  235. for (let wkt of item.wktArr) {
  236. const f = new Feature({
  237. geometry: new WKT().readGeometry(wkt),
  238. });
  239. f.set("imgName", item.imgName);
  240. this.facilityPointLayer.source.addFeature(f);
  241. extentSource.addFeature(f);
  242. }
  243. }
  244. const extent = extentSource.getExtent();
  245. if (extent && !isNaN(extent[0])) {
  246. this.kmap.map.getView().fit(extent, {
  247. padding: [280, 400, 200, 150],
  248. duration: 500,
  249. });
  250. }
  251. }
  252. }
  253. export default DistributionLayer;