drawRegionMap.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. import * as KMap from "@/utils/ol-map/KMap";
  2. import * as util from "@/common/ol_common.js";
  3. import config from "@/api/config.js";
  4. import Style from "ol/style/Style";
  5. import Icon from "ol/style/Icon";
  6. import { Fill, Stroke, Text } from "ol/style.js";
  7. import { Point } from 'ol/geom';
  8. import Feature from "ol/Feature";
  9. import * as proj from "ol/proj";
  10. import { getArea } from 'ol/sphere.js';
  11. import WKT from "ol/format/WKT.js";
  12. import proj4 from "proj4"
  13. import { register } from "ol/proj/proj4";
  14. proj4.defs("EPSG:38572", "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs +type=crs");
  15. register(proj4);
  16. /**
  17. * @description 地图层对象
  18. */
  19. class DrawRegionMap {
  20. constructor() {
  21. let that = this;
  22. let vectorStyle = new KMap.VectorStyle();
  23. this.vectorStyle = vectorStyle;
  24. // 是否允许编辑(勾画页 true,小弹窗回显页 false)
  25. this.editable = true;
  26. // 位置图标
  27. this.clickPointLayer = new KMap.VectorLayer("clickPointLayer", 9999, {
  28. style: () => {
  29. return new Style({
  30. image: new Icon({
  31. // src: require("@/assets/img/home/garden-point.png"),
  32. src: require("@/assets/img/map/map_point.png"),
  33. scale: 0.5,
  34. // anchor: [0.5, 0.5],
  35. }),
  36. });
  37. },
  38. });
  39. // 只读状态区域图层(展示多 polygon,用于“已解决 / 未解决”等状态)
  40. // 层级需低于默认可编辑 polygonLayer(1000),避免标签遮挡当前编辑地块
  41. this.staticRegionLayer = new KMap.VectorLayer("staticRegionLayer", 900, {
  42. style: (f) => {
  43. const displayMode = f.get("displayMode");
  44. // 品种勾画页:已勾画的其他品种仅做只读灰色展示,并显示品种名
  45. if (displayMode === "readonlyVariety") {
  46. // 品种区只读展示:统一使用品种区视觉样式
  47. return new Style({
  48. fill: new Fill({ color: "rgba(0, 57, 44, 0.5)" }),
  49. stroke: new Stroke({ color: "#18AA8B", width: 1.5 }),
  50. text: new Text({
  51. text: f.get("label") || "",
  52. font: "12px sans-serif",
  53. fill: new Fill({ color: "#ffffff" }),
  54. backgroundFill: new Fill({ color: "rgba(0, 57, 44, 0.85)" }),
  55. padding: [2, 6, 2, 6],
  56. }),
  57. });
  58. }
  59. const status = f.get("status"); // 'resolved' | 'unresolved'
  60. const reproductiveName = f.get("reproductiveName") || "";
  61. const isResolved = status === "resolved";
  62. const unresolvedBlueFill = f.get("unresolvedBlueFill") === true;
  63. // 已解决:深灰填充,浅白描边;未解决:默认深灰半透明 + 蓝描边;unresolvedBlueFill 时浅蓝填充(互动列表等)
  64. const fillColor = isResolved
  65. ? "rgba(0, 0, 0, 0.6)"
  66. : unresolvedBlueFill
  67. ? "rgba(33, 153, 248, 0.35)"
  68. : "rgba(0, 0, 0, 0.5)";
  69. const strokeColor = isResolved ? "#7C7C7C" : "#2199F8";
  70. const text = new Text({
  71. text: reproductiveName ? `${reproductiveName} ${status === "resolved" ? "已解决" : "未解决"}` : status === "resolved" ? "已解决" : "未解决",
  72. font: "12px sans-serif",
  73. fill: new Fill({ color: "#ffffff" }),
  74. backgroundFill: new Fill({
  75. color: isResolved ? "#949494" : "#2199F8"
  76. }),
  77. padding: [1, 5, 1, 5],
  78. });
  79. const style = new Style({
  80. fill: new Fill({ color: fillColor }),
  81. stroke: new Stroke({ color: strokeColor, width: 1 }),
  82. text: text,
  83. })
  84. const text2 = new Style({
  85. text: new Text({
  86. text: `发现时间:${f.get("updatedTime")}`,
  87. font: "12px sans-serif",
  88. offsetY: -24,
  89. fill: new Fill({ color: "#ffffff" }),
  90. backgroundFill: new Fill({
  91. color: isResolved ? "rgba(171, 171, 171, 0.4)" : "rgba(33, 153, 248, 0.6)"
  92. }),
  93. padding: [1, 5, 1, 5],
  94. }),
  95. });
  96. return [style, text2];
  97. },
  98. });
  99. }
  100. /**
  101. * 初始化地图
  102. * @param {string} location WKT 点位
  103. * @param {HTMLElement|string} target 地图容器
  104. * @param {boolean} editable 是否允许绘制/编辑地块
  105. * @param {boolean} movable 是否允许拖动/缩放地图
  106. * @param {boolean} showPoint 是否显示初始点位图标
  107. * @param {Function} onDrawEnd 绘制闭环回调(drawend),可选
  108. */
  109. initMap(location, target, editable = true, movable = true, showPoint = true, onDrawEnd) {
  110. let level = 16;
  111. let coordinate = util.wktCastGeom(location).getFirstCoordinate();
  112. this.kmap = new KMap.Map(target, level, coordinate[0], coordinate[1], null, 8, 22);
  113. // 记录当前地图是否可编辑,供样式控制使用
  114. this.editable = editable;
  115. let xyz2 = config.base_img_url3 + "map/lby/{z}/{x}/{y}.png";
  116. this.kmap.addXYZLayer(xyz2, { minZoom: 8, maxZoom: 22 }, 2);
  117. this.kmap.addLayer(this.clickPointLayer.layer);
  118. this.kmap.addLayer(this.staticRegionLayer.layer);
  119. // 根据 showPoint 决定是否在初始化时落下点位图标
  120. if (showPoint) {
  121. this.setMapPoint(coordinate);
  122. } else {
  123. this.clickPointLayer.source.clear();
  124. }
  125. // 仅在 editable 为 true 时开启绘制/编辑能力(用于勾画页面)
  126. if (editable) {
  127. this.kmap.initDraw((e) => {
  128. if (typeof onDrawEnd === "function") {
  129. try {
  130. onDrawEnd(e);
  131. } catch (_) {
  132. // 回调失败不影响地图继续使用
  133. }
  134. }
  135. });
  136. this.kmap.startDraw()
  137. this.kmap.modifyDraw()
  138. }
  139. // movable 为 false 时,禁用地图拖动、缩放等交互(用于小弹窗只看不动)
  140. if (!movable && this.kmap && this.kmap.setStates) {
  141. this.kmap.setStates({
  142. DoubleClickZoom: false,
  143. DragAndDrop: false,
  144. MouseWheelZoom: false,
  145. });
  146. }
  147. }
  148. /**
  149. * 回显地块
  150. * @param {string[]} geometryArr 多边形 WKT 数组
  151. * @param {boolean} needFitView 是否自动缩放视图
  152. * @param {string|number} areaText 显示的面积(单位:亩),可选
  153. * @param {{ fill?: string, stroke?: string }} [readonlyAreaStyle] 只读模式下覆盖填充/描边色;不传则仍用 Map.drawStyleColors 或默认
  154. * @param {{ badgeText: string, discoveryDate: string, badgeBackground?: string }} [growthOverlay] 只读模式下异常区标签(配色与勾画页一致)
  155. * @param {string} [polygonCenterLabel] 只读模式下覆盖多边形中心文案(原样展示,不加「亩」);用于品种查看态显示品种名等
  156. */
  157. setAreaGeometry(geometryArr, needFitView = false, areaText, readonlyAreaStyle, growthOverlay, polygonCenterLabel) {
  158. // 兜底保护:geometryArr 可能为 undefined/null 或空数组
  159. if (!Array.isArray(geometryArr) || geometryArr.length === 0) return;
  160. // 地图实例或图层尚未初始化时也直接返回,避免报错
  161. if (!this.kmap || !this.kmap.polygonLayer || !this.kmap.polygonLayer.source) return;
  162. let that = this;
  163. geometryArr.map(item => {
  164. // 不使用 setLayerWkt,而是手动添加要素,避免自动缩放视图
  165. const format = new WKT()
  166. const mapProjection = that.kmap.map.getView().getProjection()
  167. let geometry = format.readGeometry(item, {
  168. dataProjection: 'EPSG:4326',
  169. featureProjection: mapProjection
  170. })
  171. let f = new Feature({ geometry: geometry })
  172. // 只读模式下,为多边形单独设置样式:仅填充+边框 + 面积文本,不显示可拖动的顶点小圆点
  173. if (!this.editable) {
  174. // 查看模式下单块区域展示:优先 readonlyAreaStyle,其次 Map.drawStyleColors,再默认
  175. const fillColor =
  176. readonlyAreaStyle?.fill ??
  177. KMap.Map?.drawStyleColors?.fill ??
  178. "rgba(0, 57, 44, 0.5)";
  179. const strokeColor =
  180. readonlyAreaStyle?.stroke ??
  181. KMap.Map?.drawStyleColors?.stroke ??
  182. "#18AA8B";
  183. const styles = [
  184. new Style({
  185. fill: new Fill({
  186. color: fillColor,
  187. }),
  188. stroke: new Stroke({
  189. color: strokeColor,
  190. width: 2,
  191. }),
  192. }),
  193. ];
  194. // 中心文本:优先 polygonCenterLabel(品种查看态显示名称);否则按面积规则
  195. const trimmedCenterLabel =
  196. typeof polygonCenterLabel === "string" && polygonCenterLabel.trim() !== ""
  197. ? polygonCenterLabel.trim()
  198. : "";
  199. // 面积文本显示规则:
  200. // 1) 传空字符串:不显示
  201. // 2) 传了值:优先显示传入值
  202. // 3) 未传:按当前地块实时计算亩数
  203. const isExplicitEmptyText = typeof areaText === "string" && areaText.trim() === "";
  204. const hasProvidedAreaText = areaText !== undefined && areaText !== null;
  205. let textValue = "";
  206. if (trimmedCenterLabel) {
  207. textValue = trimmedCenterLabel;
  208. } else if (!isExplicitEmptyText) {
  209. if (hasProvidedAreaText) {
  210. textValue = `${areaText}亩`;
  211. } else {
  212. try {
  213. let geom = geometry.clone();
  214. geom.transform(proj.get("EPSG:4326"), proj.get("EPSG:38572"));
  215. let areaItem = getArea(geom);
  216. areaItem = (areaItem + areaItem / 2) / 1000;
  217. textValue = `${Number(areaItem).toFixed(2)}亩`;
  218. } catch (_) {
  219. textValue = "";
  220. }
  221. }
  222. }
  223. const hasGrowth =
  224. growthOverlay &&
  225. growthOverlay.badgeText &&
  226. growthOverlay.discoveryDate;
  227. if (textValue) {
  228. // 品种名与 staticRegionLayer.readonlyVariety 标签视觉一致(12px + 深绿底)
  229. const isVarietyNameLabel = !!trimmedCenterLabel;
  230. styles.push(
  231. new Style({
  232. text: new Text({
  233. text: textValue,
  234. font: isVarietyNameLabel ? "12px sans-serif" : "15px sans-serif",
  235. fill: new Fill({ color: "#ffffff" }),
  236. ...(isVarietyNameLabel
  237. ? {
  238. backgroundFill: new Fill({
  239. color: "rgba(0, 57, 44, 0.85)",
  240. }),
  241. padding: [2, 6, 2, 6],
  242. }
  243. : {}),
  244. offsetY: hasGrowth ? 14 : 0,
  245. }),
  246. })
  247. );
  248. }
  249. if (hasGrowth) {
  250. styles.push(
  251. new Style({
  252. text: new Text({
  253. text: growthOverlay.badgeText,
  254. font: "bold 13px sans-serif",
  255. fill: new Fill({ color: "#ffffff" }),
  256. backgroundFill: new Fill({
  257. color: growthOverlay.badgeBackground || "#FF7F00",
  258. }),
  259. padding: [4, 10, 4, 10],
  260. offsetY: -40,
  261. }),
  262. }),
  263. new Style({
  264. text: new Text({
  265. text: `发现时间:${growthOverlay.discoveryDate}`,
  266. font: "12px sans-serif",
  267. fill: new Fill({ color: "#ffffff" }),
  268. offsetY: -16,
  269. }),
  270. })
  271. );
  272. }
  273. f.setStyle(styles);
  274. }
  275. that.kmap.polygonLayer.source.addFeature(f)
  276. })
  277. // 根据参数决定是否需要自适应地块范围
  278. if (needFitView) {
  279. this.fitView()
  280. }
  281. }
  282. fitView() {
  283. if (!this.kmap?.polygonLayer?.source) return;
  284. const extent = this.kmap.polygonLayer.source.getExtent();
  285. if (
  286. !extent ||
  287. !isFinite(extent[0]) ||
  288. !isFinite(extent[1]) ||
  289. !isFinite(extent[2]) ||
  290. !isFinite(extent[3])
  291. ) {
  292. return;
  293. }
  294. this.kmap.getView().fit(extent, { duration: 500, padding: [10, 10, 10, 10] });
  295. }
  296. clearLayer() {
  297. // this.kmap.removeLayer(this.clickPointLayer.layer)
  298. if (this.kmap && this.kmap.polygonLayer) {
  299. this.kmap.polygonLayer.source.clear();
  300. }
  301. if (this.staticRegionLayer && this.staticRegionLayer.source) {
  302. this.staticRegionLayer.source.clear();
  303. }
  304. }
  305. /** 取消当前未完成的勾画(草图在 Draw 的 overlay 上,仅 clear 多边形图层去不掉) */
  306. abortOngoingDrawSketch() {
  307. if (!this.kmap || !this.kmap.draw || typeof this.kmap.draw.abortDrawing !== "function") return;
  308. this.kmap.draw.abortDrawing();
  309. }
  310. /**
  311. * 销毁地图实例(用于从编辑态切换到仅查看时重新初始化)
  312. */
  313. destroyMap() {
  314. this.clearLayer();
  315. if (this.kmap && typeof this.kmap.destroy === "function") {
  316. this.kmap.destroy();
  317. }
  318. this.kmap = null;
  319. }
  320. getAreaGeometry() {
  321. const features = this.kmap.getLayerFeatures()
  322. console.log(features, 'features');
  323. let geometryArr = []
  324. let area = 0
  325. const format = new WKT()
  326. // 获取图层上的Polygon,转成WKT用于回显
  327. features.forEach(item => {
  328. console.log(item, 'item');
  329. // 使用 writeGeometry 而不是 writeFeature,因为 setLayerWkt 期望的是几何体的 WKT
  330. const geometry = item.getGeometry()
  331. geometryArr.push(format.writeGeometry(geometry, {
  332. dataProjection: 'EPSG:4326',
  333. featureProjection: this.kmap.map.getView().getProjection()
  334. }))
  335. let geom = geometry.clone()
  336. geom.transform(proj.get("EPSG:4326"), proj.get("EPSG:38572"))
  337. let areaItem = getArea(geom)
  338. areaItem = (areaItem + areaItem / 2) / 1000;
  339. area += areaItem
  340. })
  341. return { geometryArr, mianji: area.toFixed(2) } // 修改为 mianji 字段,与创建页面保持一致
  342. }
  343. setMapPosition(center) {
  344. this.kmap.getView().animate({
  345. center,
  346. zoom: 17,
  347. duration: 500,
  348. });
  349. this.setMapPoint(center)
  350. }
  351. setMapPoint(coordinate) {
  352. this.clickPointLayer.source.clear()
  353. let point = new Feature(new Point(coordinate))
  354. this.clickPointLayer.addFeature(point)
  355. }
  356. // 删除当前地块(删除最新绘制的一个地块)
  357. deleteCurrentPolygon() {
  358. if (!this.kmap || !this.kmap.polygonLayer) return;
  359. const features = this.kmap.polygonLayer.source.getFeatures();
  360. if (features && features.length > 0) {
  361. const lastFeature = features[features.length - 1];
  362. this.kmap.polygonLayer.source.removeFeature(lastFeature);
  363. }
  364. }
  365. /**
  366. * 设置只读状态区域图层(多个 polygon,不可编辑)
  367. * @param {Array<{ geometry: string, status?: 'resolved' | 'unresolved', label?: string, displayMode?: string, unresolvedBlueFill?: boolean }>} regions
  368. *
  369. * 使用示例:
  370. * drawRegionMap.setStatusRegions([
  371. * { geometry: 'MULTIPOLYGON(((...)))', status: 'resolved' },
  372. * { geometry: 'MULTIPOLYGON(((...)))', status: 'unresolved' },
  373. * ]);
  374. */
  375. setStatusRegions(regions) {
  376. if (!this.kmap || !this.staticRegionLayer || !this.staticRegionLayer.source) return;
  377. // 仅操作只读图层,不影响当前地图的绘制 / 编辑状态
  378. this.staticRegionLayer.source.clear();
  379. if (!Array.isArray(regions) || regions.length === 0) return;
  380. const format = new WKT();
  381. const mapProjection = this.kmap.map.getView().getProjection();
  382. regions.forEach((region) => {
  383. if (!region || !region.geometry) return;
  384. try {
  385. const geometry = format.readGeometry(region.geometry, {
  386. dataProjection: "EPSG:4326",
  387. featureProjection: mapProjection,
  388. });
  389. const feature = new Feature({ geometry });
  390. feature.set("reproductiveName", region.reproductiveName);
  391. feature.set("status", region.status || "unresolved");
  392. feature.set("updatedTime", region.updatedTime);
  393. feature.set("label", region.label || "");
  394. feature.set("displayMode", region.displayMode || "");
  395. feature.set("unresolvedBlueFill", region.unresolvedBlueFill === true);
  396. this.staticRegionLayer.addFeature(feature);
  397. } catch (e) {
  398. // 单个区域解析失败时忽略
  399. }
  400. });
  401. }
  402. /**
  403. * 视图自适应到「只读区域图层 + 可编辑多边形图层」的联合范围
  404. * 适用于同时存在接口返回区域和本地勾画区域时,保证都能出现在视野内
  405. */
  406. fitAllRegions() {
  407. if (!this.kmap) return;
  408. const extents = [];
  409. // 只读状态区域图层范围
  410. if (this.staticRegionLayer && this.staticRegionLayer.source) {
  411. const features = this.staticRegionLayer.source.getFeatures();
  412. if (features && features.length > 0) {
  413. extents.push(this.staticRegionLayer.source.getExtent());
  414. }
  415. }
  416. // 可编辑 polygon 图层范围
  417. if (this.kmap.polygonLayer && this.kmap.polygonLayer.source) {
  418. const features = this.kmap.polygonLayer.source.getFeatures();
  419. if (features && features.length > 0) {
  420. extents.push(this.kmap.polygonLayer.source.getExtent());
  421. }
  422. }
  423. if (extents.length === 0) return;
  424. // 计算所有范围的并集 [minX, minY, maxX, maxY]
  425. const merged = extents.reduce(
  426. (acc, cur) => {
  427. if (!acc) return cur.slice();
  428. return [
  429. Math.min(acc[0], cur[0]),
  430. Math.min(acc[1], cur[1]),
  431. Math.max(acc[2], cur[2]),
  432. Math.max(acc[3], cur[3]),
  433. ];
  434. },
  435. null
  436. );
  437. if (merged) {
  438. this.kmap.getView().fit(merged, { duration: 500, padding: [100, 100, 100, 100] });
  439. }
  440. }
  441. }
  442. export default DrawRegionMap;