mapManage.js 35 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147
  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 from "ol/style/Fill";
  7. import Stroke from "ol/style/Stroke";
  8. import Text from "ol/style/Text";
  9. import { Point, Polygon, MultiPolygon } from "ol/geom";
  10. import Feature from "ol/Feature";
  11. import DragPan from "ol/interaction/DragPan";
  12. import MouseWheelZoom from "ol/interaction/MouseWheelZoom";
  13. import PinchZoom from "ol/interaction/PinchZoom";
  14. import PinchRotate from "ol/interaction/PinchRotate";
  15. import DoubleClickZoom from "ol/interaction/DoubleClickZoom";
  16. import KeyboardPan from "ol/interaction/KeyboardPan";
  17. import KeyboardZoom from "ol/interaction/KeyboardZoom";
  18. import DragRotateAndZoom from "ol/interaction/DragRotateAndZoom";
  19. import Select from "ol/interaction/Select";
  20. import { singleClick } from "ol/events/condition";
  21. import { reactive } from "vue";
  22. import WKT from "ol/format/WKT.js";
  23. import GeoJSON from "ol/format/GeoJSON";
  24. import * as proj from "ol/proj";
  25. import { getArea } from "ol/sphere.js";
  26. import * as turf from "@turf/turf";
  27. const DEFAULT_ZONE_STYLE = {
  28. fill: "rgba(100, 0, 0, 0.45)",
  29. fillSelected: "rgba(100, 0, 0, 0.5)",
  30. stroke: "#E03131",
  31. };
  32. const VIEWPORT_INTERACTION_TYPES = [
  33. DragPan,
  34. MouseWheelZoom,
  35. PinchZoom,
  36. PinchRotate,
  37. DoubleClickZoom,
  38. KeyboardPan,
  39. KeyboardZoom,
  40. DragRotateAndZoom,
  41. ];
  42. function setViewportInteractionsActive(olMap, active) {
  43. olMap.getInteractions().forEach((ix) => {
  44. if (VIEWPORT_INTERACTION_TYPES.some((T) => ix instanceof T)) {
  45. ix.setActive(active);
  46. }
  47. });
  48. }
  49. export let mapLocation = reactive({
  50. data: null,
  51. });
  52. /**
  53. * @description 地图层对象
  54. */
  55. class MapManage {
  56. constructor() {
  57. let vectorStyle = new KMap.VectorStyle();
  58. this.vectorStyle = vectorStyle;
  59. this.regionDrawingActive = false;
  60. this.clickPointLayer = new KMap.VectorLayer("clickPointLayer", 9999, {
  61. style: () => {
  62. return new Style({
  63. image: new Icon({
  64. src: require("@/assets/img/home/garden-point.png"),
  65. scale: 0.5,
  66. anchor: [0.5, 0.5],
  67. }),
  68. });
  69. },
  70. });
  71. this.boundaryLayer = new KMap.VectorLayer("drawBoundaryLayer", 1050, {
  72. style: () => this.createBoundaryStyle(),
  73. });
  74. this.boundaryGeometry = null;
  75. this.constrainedDrawing = false;
  76. this.constrainedDrawingReady = false;
  77. this.zoneStyle = { ...DEFAULT_ZONE_STYLE };
  78. this.gridLayer = new KMap.VectorLayer("terrainGridLayer", 1100, {
  79. style: (feature) => {
  80. const selected = !!feature.get("selected");
  81. return new Style({
  82. fill: new Fill({
  83. color: selected ? this.zoneStyle.fillSelected : "rgba(255, 255, 255, 0.01)",
  84. }),
  85. stroke: new Stroke({
  86. color: selected ? this.zoneStyle.stroke : "#fff",
  87. width: selected ? 1.8 : 1.2,
  88. }),
  89. });
  90. },
  91. });
  92. this.existingVarietyLayer = new KMap.VectorLayer("existingVarietyLayer", 1080, {
  93. style: (feature) => this.createExistingVarietyStyle(feature),
  94. });
  95. this.gridToggleSelect = null;
  96. this.selectedGridIds = new Set();
  97. this.terrainGridItems = [];
  98. this.wktFormat = new WKT();
  99. this.editable = true;
  100. }
  101. createReadonlyPolygonStyle() {
  102. return new Style({
  103. fill: new Fill({
  104. color: "rgba(124, 124, 124, 0.5)",
  105. }),
  106. stroke: new Stroke({
  107. color: "rgba(255, 255, 255, 0.55)",
  108. width: 2,
  109. }),
  110. });
  111. }
  112. createExistingVarietyStyle(feature) {
  113. const name = String(feature.get("name") || "");
  114. const fill = feature.get("fill") || "rgba(76, 175, 80, 0.28)";
  115. const stroke = feature.get("stroke") || "#4CAF50";
  116. return [
  117. new Style({
  118. fill: new Fill({ color: fill }),
  119. stroke: new Stroke({
  120. color: stroke,
  121. width: 2,
  122. }),
  123. }),
  124. new Style({
  125. text: new Text({
  126. text: name,
  127. font: "13px sans-serif",
  128. fill: new Fill({ color: "#1F1F1F" }),
  129. backgroundFill: new Fill({ color: "#ffffff" }),
  130. padding: [4, 8, 4, 8],
  131. overflow: true,
  132. }),
  133. }),
  134. ];
  135. }
  136. createBoundaryStyle() {
  137. return new Style({
  138. fill: new Fill({
  139. color: "rgba(124, 124, 124, 0.12)",
  140. }),
  141. stroke: new Stroke({
  142. color: "rgba(255, 255, 255, 0.85)",
  143. width: 2,
  144. lineDash: [8, 4],
  145. }),
  146. });
  147. }
  148. createDrawnZoneStyle() {
  149. return new Style({
  150. fill: new Fill({
  151. color: this.zoneStyle.fill,
  152. }),
  153. stroke: new Stroke({
  154. color: this.zoneStyle.stroke,
  155. width: 1.8,
  156. }),
  157. });
  158. }
  159. initMap(location, target, options = {}) {
  160. const {
  161. editable = true,
  162. constrainedDrawing = false,
  163. onDrawOutsideBoundary,
  164. zoneStyle,
  165. preventOverlapExisting = false,
  166. onDrawOverlap,
  167. singleDraw = false,
  168. onDrawLimit,
  169. } = options;
  170. this.zoneStyle = zoneStyle ? { ...DEFAULT_ZONE_STYLE, ...zoneStyle } : { ...DEFAULT_ZONE_STYLE };
  171. this.editable = editable;
  172. this.constrainedDrawing = constrainedDrawing;
  173. this.preventOverlapExisting = !!preventOverlapExisting;
  174. this.singleDraw = !!singleDraw;
  175. this.onDrawOutsideBoundary = typeof onDrawOutsideBoundary === "function" ? onDrawOutsideBoundary : null;
  176. this.onDrawOverlap = typeof onDrawOverlap === "function" ? onDrawOverlap : null;
  177. this.onDrawLimit = typeof onDrawLimit === "function" ? onDrawLimit : null;
  178. this.constrainedDrawingReady = false;
  179. this.overlapConstraintReady = false;
  180. this.boundaryGeometry = null;
  181. let level = 16;
  182. let coordinate = util.wktCastGeom(location).getFirstCoordinate();
  183. this.kmap = new KMap.Map(target, level, coordinate[0], coordinate[1], null, 8, 22);
  184. let xyz2 = config.base_img_url3 + "map/lby/{z}/{x}/{y}.png";
  185. this.kmap.addXYZLayer(xyz2, { minZoom: 8, maxZoom: 22 }, 2);
  186. // this.kmap.addLayer(this.clickPointLayer.layer);
  187. this.kmap.addLayer(this.boundaryLayer.layer);
  188. this.kmap.addLayer(this.existingVarietyLayer.layer);
  189. this.kmap.addLayer(this.gridLayer.layer);
  190. if (this.editable) {
  191. this.kmap.initDraw((e) => {
  192. if (this.preventOverlapExisting && e?.feature) {
  193. this.handleDrawEndOverlap(e.feature);
  194. }
  195. this.syncSingleDrawState();
  196. });
  197. this.kmap.modifyDraw();
  198. this.setupOverlapConstraintListeners();
  199. this.setupSingleDrawListeners();
  200. this.setRegionDrawingActive(false);
  201. } else if (this.constrainedDrawing) {
  202. this.setupConstrainedDrawing();
  203. this.setConstrainedDrawingActive(false);
  204. }
  205. }
  206. getDrawnFeatureCount() {
  207. return this.kmap?.polygonLayer?.source?.getFeatures?.()?.length || 0;
  208. }
  209. /** 单范围模式:已有勾画则关闭继续绘制,仍可拖拽编辑 */
  210. syncSingleDrawState() {
  211. if (!this.singleDraw || !this.kmap?.draw) return;
  212. const canDraw = this.regionDrawingActive && this.getDrawnFeatureCount() === 0;
  213. this.kmap.draw.setActive(canDraw);
  214. }
  215. setupSingleDrawListeners() {
  216. if (!this.kmap?.draw || !this.singleDraw) return;
  217. this.kmap.draw.on("drawstart", () => {
  218. if (!this.singleDraw) return;
  219. if (this.getDrawnFeatureCount() === 0) return;
  220. if (typeof this.kmap.draw.abortDrawing === "function") {
  221. this.kmap.draw.abortDrawing();
  222. }
  223. this.onDrawLimit?.();
  224. this.syncSingleDrawState();
  225. });
  226. }
  227. notifyDrawOverlap() {
  228. this.onDrawOverlap?.();
  229. }
  230. toTurfFeature(geometry) {
  231. if (!geometry || !this.kmap) return null;
  232. const geoJson = new GeoJSON();
  233. const projection = this.kmap.map.getView().getProjection();
  234. try {
  235. return turf.feature(
  236. geoJson.writeGeometryObject(geometry, {
  237. dataProjection: "EPSG:4326",
  238. featureProjection: projection,
  239. })
  240. );
  241. } catch {
  242. return null;
  243. }
  244. }
  245. /** 点是否落在已有品种范围内 */
  246. isCoordinateInExistingVariety(coordinate) {
  247. if (!coordinate || !this.existingVarietyLayer?.source) return false;
  248. return this.existingVarietyLayer.source.getFeatures().some((feature) => {
  249. try {
  250. return !!feature.getGeometry()?.intersectsCoordinate(coordinate);
  251. } catch {
  252. return false;
  253. }
  254. });
  255. }
  256. /**
  257. * 几何是否与禁止区域重叠(已有品种 + 其他已勾画面,可排除自身)
  258. * 仅面积相交算重叠,边界相接允许
  259. */
  260. geometryOverlapsForbidden(geometry, excludeFeature = null) {
  261. if (!geometry || !this.kmap) return false;
  262. const drawn = this.toTurfFeature(geometry);
  263. if (!drawn) return false;
  264. const overlaps = (otherFeature) => {
  265. const otherGeom = otherFeature.getGeometry();
  266. if (!otherGeom) return false;
  267. const other = this.toTurfFeature(otherGeom);
  268. if (!other) return false;
  269. try {
  270. let result = null;
  271. try {
  272. result = turf.intersect(turf.featureCollection([drawn, other]));
  273. } catch {
  274. result = turf.intersect(drawn, other);
  275. }
  276. if (!result?.geometry) return false;
  277. return turf.area(result) > 0.5;
  278. } catch {
  279. return false;
  280. }
  281. };
  282. const checkFeatures = (features) =>
  283. features.some((feature) => {
  284. if (excludeFeature && feature === excludeFeature) return false;
  285. return overlaps(feature);
  286. });
  287. if (checkFeatures(this.existingVarietyLayer?.source?.getFeatures?.() || [])) {
  288. return true;
  289. }
  290. if (checkFeatures(this.kmap.polygonLayer?.source?.getFeatures?.() || [])) {
  291. return true;
  292. }
  293. return false;
  294. }
  295. handleDrawEndOverlap(feature) {
  296. if (!feature) return;
  297. const geometry = feature.getGeometry();
  298. if (!this.geometryOverlapsForbidden(geometry, feature)) return;
  299. if (this.kmap?.polygonLayer?.source) {
  300. this.kmap.polygonLayer.source.removeFeature(feature);
  301. }
  302. this.notifyDrawOverlap();
  303. }
  304. handleModifyEndOverlap(feature) {
  305. if (!feature) return;
  306. const geometry = feature.getGeometry();
  307. if (!this.geometryOverlapsForbidden(geometry, feature)) {
  308. feature.unset("_preModifyGeom");
  309. return;
  310. }
  311. const prev = feature.get("_preModifyGeom");
  312. if (prev) {
  313. feature.setGeometry(prev);
  314. } else if (this.kmap?.polygonLayer?.source) {
  315. this.kmap.polygonLayer.source.removeFeature(feature);
  316. }
  317. feature.unset("_preModifyGeom");
  318. this.notifyDrawOverlap();
  319. }
  320. bindSketchOverlapConstraint(sketch) {
  321. if (!sketch) return;
  322. const onSketchChange = () => {
  323. if (!this.preventOverlapExisting) return;
  324. const geometry = sketch.getGeometry();
  325. if (!geometry) return;
  326. const type = geometry.getType();
  327. if (type !== "LineString" && type !== "Point") return;
  328. const last = this.getLastCoordinateFromGeometry(geometry);
  329. if (last && this.isCoordinateInExistingVariety(last)) {
  330. sketch.getGeometry()?.un("change", onSketchChange);
  331. if (this.kmap?.draw && typeof this.kmap.draw.abortDrawing === "function") {
  332. this.kmap.draw.abortDrawing();
  333. }
  334. this.notifyDrawOverlap();
  335. }
  336. };
  337. sketch.getGeometry()?.on("change", onSketchChange);
  338. const cleanup = () => sketch.getGeometry()?.un("change", onSketchChange);
  339. this.kmap.draw.once("drawend", cleanup);
  340. this.kmap.draw.once("drawabort", cleanup);
  341. }
  342. setupOverlapConstraintListeners() {
  343. if (!this.kmap?.draw || this.overlapConstraintReady) return;
  344. this.overlapConstraintReady = true;
  345. this.kmap.draw.on("drawstart", (e) => {
  346. if (!this.preventOverlapExisting) return;
  347. const coordinate = e.coordinate || this.getLastCoordinateFromGeometry(e.feature?.getGeometry());
  348. if (coordinate && this.isCoordinateInExistingVariety(coordinate)) {
  349. if (this.kmap?.draw && typeof this.kmap.draw.abortDrawing === "function") {
  350. this.kmap.draw.abortDrawing();
  351. }
  352. this.notifyDrawOverlap();
  353. return;
  354. }
  355. this.bindSketchOverlapConstraint(e.feature);
  356. });
  357. if (this.kmap.modify) {
  358. this.kmap.modify.on("modifystart", (e) => {
  359. if (!this.preventOverlapExisting) return;
  360. e.features?.forEach((feature) => {
  361. const geom = feature.getGeometry();
  362. if (geom) feature.set("_preModifyGeom", geom.clone());
  363. });
  364. });
  365. this.kmap.modify.on("modifyend", (e) => {
  366. if (!this.preventOverlapExisting) return;
  367. e.features?.forEach((feature) => this.handleModifyEndOverlap(feature));
  368. });
  369. }
  370. }
  371. /** 当前勾画面是否与已有品种范围重叠(确认前提交校验) */
  372. hasDrawnOverlapWithExisting() {
  373. if (!this.kmap?.polygonLayer?.source) return false;
  374. return this.kmap.polygonLayer.source.getFeatures().some((feature) =>
  375. this.geometryOverlapsForbidden(feature.getGeometry(), feature)
  376. );
  377. }
  378. isCoordinateInsideBoundary(coordinate) {
  379. if (!this.boundaryGeometry || !coordinate || !this.kmap) return false;
  380. try {
  381. return this.boundaryGeometry.intersectsCoordinate(coordinate);
  382. } catch {
  383. return false;
  384. }
  385. }
  386. notifyDrawOutsideBoundary() {
  387. this.onDrawOutsideBoundary?.();
  388. }
  389. getLastCoordinateFromGeometry(geometry) {
  390. if (!geometry || typeof geometry.getType !== "function") return null;
  391. const type = geometry.getType();
  392. if (type === "Point") return geometry.getCoordinates();
  393. if (type === "LineString") {
  394. const coords = geometry.getCoordinates();
  395. return coords.length ? coords[coords.length - 1] : null;
  396. }
  397. if (type === "Polygon") {
  398. const ring = geometry.getCoordinates()[0];
  399. return ring?.length ? ring[ring.length - 1] : null;
  400. }
  401. if (type === "MultiPolygon") {
  402. const polys = geometry.getCoordinates();
  403. const ring = polys[polys.length - 1]?.[0];
  404. return ring?.length ? ring[ring.length - 1] : null;
  405. }
  406. return null;
  407. }
  408. /** 区域外勾画:中止当前绘制并移除无效地块 */
  409. rejectOutsideDraw(feature) {
  410. if (feature && this.kmap?.polygonLayer?.source) {
  411. this.kmap.polygonLayer.source.removeFeature(feature);
  412. }
  413. if (this.kmap?.draw && typeof this.kmap.draw.abortDrawing === "function") {
  414. this.kmap.draw.abortDrawing();
  415. }
  416. this.notifyDrawOutsideBoundary();
  417. }
  418. unbindSketchBoundaryConstraint(sketch, listener) {
  419. sketch?.getGeometry()?.un("change", listener);
  420. }
  421. bindSketchBoundaryConstraint(sketch) {
  422. const onSketchChange = () => {
  423. const geometry = sketch.getGeometry();
  424. if (!geometry) return;
  425. const type = geometry.getType();
  426. // 仅在手绘轨迹(线)阶段判断越界;面要素未完成时 intersect 会误判
  427. if (type !== "LineString" && type !== "Point") return;
  428. const last = this.getLastCoordinateFromGeometry(geometry);
  429. if (last && !this.isCoordinateInsideBoundary(last)) {
  430. this.unbindSketchBoundaryConstraint(sketch, onSketchChange);
  431. this.rejectOutsideDraw();
  432. }
  433. };
  434. sketch.getGeometry()?.on("change", onSketchChange);
  435. const cleanup = () => this.unbindSketchBoundaryConstraint(sketch, onSketchChange);
  436. this.kmap.draw.once("drawend", cleanup);
  437. this.kmap.draw.once("drawabort", cleanup);
  438. }
  439. setupConstrainedDrawing() {
  440. if (!this.kmap || this.constrainedDrawingReady) return;
  441. this.constrainedDrawingReady = true;
  442. this.kmap.initDraw((e) => {
  443. if (!e.feature) return;
  444. if (!this.clipFeatureToBoundary(e.feature)) {
  445. if (this.kmap?.polygonLayer?.source) {
  446. this.kmap.polygonLayer.source.removeFeature(e.feature);
  447. }
  448. this.notifyDrawOutsideBoundary();
  449. }
  450. });
  451. this.kmap.draw.on("drawstart", (e) => {
  452. const coordinate = e.coordinate || this.getLastCoordinateFromGeometry(e.feature?.getGeometry());
  453. if (!coordinate || !this.isCoordinateInsideBoundary(coordinate)) {
  454. this.rejectOutsideDraw();
  455. return;
  456. }
  457. this.bindSketchBoundaryConstraint(e.feature);
  458. });
  459. this.kmap.modifyDraw((e) => {
  460. e.features.forEach((feature) => {
  461. if (!this.clipFeatureToBoundary(feature)) {
  462. if (this.kmap?.polygonLayer?.source) {
  463. this.kmap.polygonLayer.source.removeFeature(feature);
  464. }
  465. this.notifyDrawOutsideBoundary();
  466. }
  467. });
  468. });
  469. }
  470. setConstrainedDrawingActive(active) {
  471. if (!this.kmap) return;
  472. if (this.kmap.draw) {
  473. this.kmap.draw.setActive(active);
  474. }
  475. if (this.kmap.modify) {
  476. this.kmap.modify.setActive(active);
  477. }
  478. }
  479. enableConstrainedDrawing() {
  480. if (!this.boundaryGeometry) return;
  481. this.setupConstrainedDrawing();
  482. this.setConstrainedDrawingActive(true);
  483. }
  484. intersectWithBoundary(geometry) {
  485. if (!this.boundaryGeometry || !geometry || !this.kmap) return null;
  486. const geoJson = new GeoJSON();
  487. const projection = this.kmap.map.getView().getProjection();
  488. const opts = {
  489. dataProjection: "EPSG:4326",
  490. featureProjection: projection,
  491. };
  492. try {
  493. const drawGeo = geoJson.writeGeometryObject(geometry, opts);
  494. const boundaryGeo = geoJson.writeGeometryObject(this.boundaryGeometry, opts);
  495. const drawnFeature = turf.feature(drawGeo);
  496. const boundaryFeature = turf.feature(boundaryGeo);
  497. let result = null;
  498. try {
  499. result = turf.intersect(turf.featureCollection([drawnFeature, boundaryFeature]));
  500. } catch {
  501. result = turf.intersect(drawnFeature, boundaryFeature);
  502. }
  503. if (!result?.geometry) return null;
  504. return geoJson.readGeometry(result.geometry, opts);
  505. } catch {
  506. return null;
  507. }
  508. }
  509. clipFeatureToBoundary(feature) {
  510. if (!feature || !this.boundaryGeometry || !this.kmap?.polygonLayer?.source) return false;
  511. const geometry = feature.getGeometry();
  512. if (!geometry) return false;
  513. const clipped = this.intersectWithBoundary(geometry);
  514. if (!clipped) {
  515. this.kmap.polygonLayer.source.removeFeature(feature);
  516. return false;
  517. }
  518. feature.setGeometry(clipped);
  519. feature.setStyle(this.createDrawnZoneStyle());
  520. return true;
  521. }
  522. setBoundaryWkt(wkt) {
  523. if (!this.kmap || !this.boundaryLayer?.source || !wkt) return;
  524. this.boundaryLayer.source.clear();
  525. const mapProjection = this.kmap.map.getView().getProjection();
  526. const geometry = this.wktFormat.readGeometry(String(wkt).trim(), {
  527. dataProjection: "EPSG:4326",
  528. featureProjection: mapProjection,
  529. });
  530. this.boundaryGeometry = geometry.clone();
  531. const feature = new Feature({ geometry });
  532. feature.set("isBoundary", true);
  533. this.boundaryLayer.addFeature(feature);
  534. this.fitBoundaryView();
  535. }
  536. clearBoundaryLayer() {
  537. this.boundaryLayer?.source?.clear();
  538. this.boundaryGeometry = null;
  539. }
  540. fitBoundaryView() {
  541. if (!this.kmap || !this.boundaryLayer?.source) return;
  542. const extent = this.boundaryLayer.source.getExtent();
  543. if (!extent || extent.some((v) => !Number.isFinite(v))) return;
  544. this.kmap.getView().fit(extent, { duration: 500, padding: [40, 40, 40, 40] });
  545. }
  546. setDrawnAreaGeometry(geometryArr) {
  547. if (!this.kmap?.polygonLayer?.source || !Array.isArray(geometryArr)) return;
  548. this.kmap.polygonLayer.source.clear();
  549. const mapProjection = this.kmap.map.getView().getProjection();
  550. geometryArr.forEach((item) => {
  551. try {
  552. const geometry = this.wktFormat.readGeometry(String(item).trim(), {
  553. dataProjection: "EPSG:4326",
  554. featureProjection: mapProjection,
  555. });
  556. const feature = new Feature({ geometry });
  557. this.clipFeatureToBoundary(feature);
  558. if (feature.getGeometry()) {
  559. this.kmap.polygonLayer.source.addFeature(feature);
  560. }
  561. } catch {
  562. /* 单条解析失败则跳过 */
  563. }
  564. });
  565. }
  566. /**
  567. * 是否允许平移/缩放、勾画与编辑;为 false 时同时隐藏中心点位图标
  568. */
  569. setRegionDrawingActive(active) {
  570. if (!this.kmap) return;
  571. this.regionDrawingActive = active;
  572. setViewportInteractionsActive(this.kmap.map, active);
  573. if (this.kmap.draw) {
  574. if (active && this.singleDraw) {
  575. this.syncSingleDrawState();
  576. } else {
  577. this.kmap.draw.setActive(active);
  578. }
  579. }
  580. if (this.kmap.modify) {
  581. this.kmap.modify.setActive(active);
  582. }
  583. if (active) {
  584. const c = this.kmap.getView().getCenter();
  585. this.setMapPoint(c);
  586. } else {
  587. // this.clickPointLayer.source.clear();
  588. }
  589. }
  590. enableRegionDrawing() {
  591. this.setRegionDrawingActive(true);
  592. }
  593. enableMapInteraction() {
  594. if (!this.kmap) return;
  595. setViewportInteractionsActive(this.kmap.map, true);
  596. }
  597. /**
  598. * 根据中心点和亩数生成正方形 WKT
  599. * @param {number[]} center [lng, lat]
  600. * @param {number} mu 面积(亩)
  601. */
  602. generateSquareWktByMu(center, mu = 60) {
  603. const lng = parseFloat(center[0]);
  604. const lat = parseFloat(center[1]);
  605. const halfSide = Math.sqrt(mu * 666.67) / 2;
  606. const latDelta = halfSide / 111000;
  607. const lngDelta = halfSide / (111000 * Math.cos((lat * Math.PI) / 180));
  608. const ring = [
  609. [lng - lngDelta, lat + latDelta],
  610. [lng + lngDelta, lat + latDelta],
  611. [lng + lngDelta, lat - latDelta],
  612. [lng - lngDelta, lat - latDelta],
  613. [lng - lngDelta, lat + latDelta],
  614. ];
  615. const coordinates = ring.map((point) => `${point[0]} ${point[1]}`).join(", ");
  616. return `MULTIPOLYGON (((${coordinates})))`;
  617. }
  618. /**
  619. * 以当前地图中心生成指定亩数的正方形区域
  620. * @param {number} mu 面积(亩)
  621. * @returns {string|null} WKT
  622. */
  623. setDefaultSquareAtCenter(mu = 60) {
  624. if (!this.kmap) return null;
  625. const center = this.kmap.getView().getCenter();
  626. const wkt = this.generateSquareWktByMu(center, mu);
  627. this.setBoundaryWkt(wkt);
  628. this.setMapPoint(center);
  629. this.enableConstrainedDrawing();
  630. return wkt;
  631. }
  632. setCenterAndSquare(center, mu = 60) {
  633. if (!this.kmap) return null;
  634. this.kmap.getView().animate({
  635. center,
  636. zoom: 16,
  637. duration: 0,
  638. });
  639. this.setMapPoint(center);
  640. const wkt = this.generateSquareWktByMu(center, mu);
  641. this.setBoundaryWkt(wkt);
  642. this.enableConstrainedDrawing();
  643. return wkt;
  644. }
  645. setMapPoint(coordinate) {
  646. // this.clickPointLayer.source.clear();
  647. let point = new Feature(new Point(coordinate));
  648. // this.clickPointLayer.addFeature(point);
  649. }
  650. showCenterMarker(coordinate, iconSrc) {
  651. if (!this.kmap || !coordinate) return;
  652. if (!this.centerMarkerLayer) {
  653. const src = iconSrc || require("@/assets/img/home/garden-point.png");
  654. this.centerMarkerLayer = new KMap.VectorLayer("centerMarkerLayer", 9998, {
  655. style: () =>
  656. new Style({
  657. image: new Icon({
  658. src,
  659. scale: 0.45,
  660. anchor: [0.5, 1],
  661. }),
  662. }),
  663. });
  664. this.kmap.addLayer(this.centerMarkerLayer.layer);
  665. }
  666. this.centerMarkerLayer.source.clear();
  667. this.centerMarkerLayer.addFeature(new Feature(new Point(coordinate)));
  668. }
  669. setMapPosition(center) {
  670. this.kmap.getView().animate({
  671. center,
  672. zoom: 16,
  673. duration: 0,
  674. });
  675. if (this.regionDrawingActive) {
  676. this.setMapPoint(center);
  677. }
  678. }
  679. clearLayer() {
  680. if (!this.kmap?.polygonLayer?.source) return;
  681. if (this.kmap.draw && typeof this.kmap.draw.abortDrawing === "function") {
  682. this.kmap.draw.abortDrawing();
  683. }
  684. this.kmap.polygonLayer.source.clear();
  685. this.clearGridLayer();
  686. this.syncSingleDrawState();
  687. }
  688. clearAllLayers() {
  689. this.clearLayer();
  690. this.clearBoundaryLayer();
  691. this.clearExistingVarietyZones();
  692. }
  693. clearExistingVarietyZones() {
  694. this.existingVarietyLayer?.source?.clear();
  695. }
  696. /**
  697. * 展示已有品种种植范围(只读),不参与勾画结果
  698. * @param {{ id?: string|number, name: string, polygon: string, fill?: string, stroke?: string }[]} zones
  699. */
  700. setExistingVarietyZones(zones = []) {
  701. if (!this.kmap || !this.existingVarietyLayer?.source) return;
  702. this.clearExistingVarietyZones();
  703. const projection = this.kmap.map.getView().getProjection();
  704. const list = Array.isArray(zones) ? zones : [];
  705. list.forEach((item) => {
  706. const wkt = item?.polygon || item?.wkt;
  707. if (!wkt) return;
  708. try {
  709. const geometry = this.wktFormat.readGeometry(String(wkt).trim(), {
  710. dataProjection: "EPSG:4326",
  711. featureProjection: projection,
  712. });
  713. const feature = new Feature({ geometry });
  714. feature.set("id", item.id);
  715. feature.set("name", item.name || "");
  716. feature.set("fill", item.fill);
  717. feature.set("stroke", item.stroke);
  718. feature.set("readonly", true);
  719. this.existingVarietyLayer.source.addFeature(feature);
  720. } catch (e) {
  721. console.warn("[MapManage] existing variety polygon parse failed", e);
  722. }
  723. });
  724. this.existingVarietyLayer.layer.changed();
  725. }
  726. clearGridLayer() {
  727. this.unbindGridClick();
  728. this.gridLayer?.source?.clear();
  729. this.selectedGridIds?.clear();
  730. this.terrainGridItems = [];
  731. }
  732. unbindGridClick() {
  733. if (this.gridToggleSelect && this.kmap?.map) {
  734. this.kmap.map.removeInteraction(this.gridToggleSelect);
  735. }
  736. this.gridToggleSelect = null;
  737. }
  738. bindGridClick() {
  739. if (!this.kmap?.map || !this.gridLayer?.layer) return;
  740. this.unbindGridClick();
  741. const selectedStyle = new Style({
  742. fill: new Fill({
  743. color: this.zoneStyle.fillSelected,
  744. }),
  745. stroke: new Stroke({
  746. color: this.zoneStyle.stroke,
  747. width: 1.8,
  748. }),
  749. });
  750. this.gridToggleSelect = new Select({
  751. condition: singleClick,
  752. toggleCondition: singleClick,
  753. layers: [this.gridLayer.layer],
  754. multi: true,
  755. hitTolerance: 8,
  756. style: selectedStyle,
  757. });
  758. this.gridToggleSelect.on("select", (e) => {
  759. e.selected.forEach((feature) => {
  760. feature.set("selected", true);
  761. this.selectedGridIds.add(feature.get("gridId"));
  762. feature.changed();
  763. });
  764. e.deselected.forEach((feature) => {
  765. feature.set("selected", false);
  766. this.selectedGridIds.delete(feature.get("gridId"));
  767. feature.changed();
  768. });
  769. this.gridLayer.layer.changed();
  770. });
  771. this.kmap.map.addInteraction(this.gridToggleSelect);
  772. }
  773. getSelectedGrids() {
  774. const empty = {
  775. gridIds: [],
  776. geometryArr: [],
  777. parcels: [],
  778. mianji: "0.00",
  779. mergedGeometry: "",
  780. };
  781. if (!this.kmap || !this.gridLayer?.source) return empty;
  782. const projection = this.kmap.map.getView().getProjection();
  783. const gridIds = [];
  784. const geometryArr = [];
  785. const parcels = [];
  786. let totalMu = 0;
  787. this.gridLayer.source.getFeatures().forEach((feature) => {
  788. if (!feature.get("selected")) return;
  789. const geometry = feature.getGeometry();
  790. if (!geometry) return;
  791. const gridId = feature.get("gridId");
  792. gridIds.push(gridId);
  793. const wkt = this.wktFormat.writeGeometry(geometry, {
  794. dataProjection: "EPSG:4326",
  795. featureProjection: projection,
  796. });
  797. geometryArr.push(wkt);
  798. let geom = geometry.clone();
  799. geom.transform(proj.get("EPSG:4326"), proj.get("EPSG:38572"));
  800. let mu = getArea(geom);
  801. mu = (mu + mu / 2) / 1000;
  802. totalMu += mu;
  803. parcels.push({ gridId, wkt, mianji: Number(mu.toFixed(2)) });
  804. });
  805. return {
  806. gridIds,
  807. geometryArr,
  808. parcels,
  809. mianji: totalMu.toFixed(2),
  810. mergedGeometry: this.mergeGeometryWkts(geometryArr),
  811. };
  812. }
  813. polygonCoordSetsFromGeometry(geometry) {
  814. if (!geometry || typeof geometry.getType !== "function") return [];
  815. const type = geometry.getType();
  816. if (type === "Polygon") return [geometry.getCoordinates()];
  817. if (type === "MultiPolygon") return geometry.getCoordinates();
  818. return [];
  819. }
  820. /**
  821. * 将多个 WKT 面合并为一个(单块返回 POLYGON,多块返回 MULTIPOLYGON)
  822. * @param {string[]} wktArr
  823. * @returns {string}
  824. */
  825. mergeGeometryWkts(wktArr) {
  826. if (!Array.isArray(wktArr) || wktArr.length === 0) return "";
  827. const trimmed = wktArr
  828. .map((item) => String(item).trim())
  829. .filter((item) => item.length > 10);
  830. if (trimmed.length === 0) return "";
  831. if (trimmed.length === 1) return trimmed[0];
  832. const wktOpts = {
  833. dataProjection: "EPSG:4326",
  834. featureProjection: "EPSG:4326",
  835. };
  836. const coordSets = [];
  837. trimmed.forEach((wkt) => {
  838. try {
  839. const geometry = this.wktFormat.readGeometry(wkt, wktOpts);
  840. coordSets.push(...this.polygonCoordSetsFromGeometry(geometry));
  841. } catch {
  842. /* 单条解析失败则跳过 */
  843. }
  844. });
  845. if (coordSets.length === 0) return trimmed[0];
  846. if (coordSets.length === 1) {
  847. return this.wktFormat.writeGeometry(new Polygon(coordSets[0]), wktOpts);
  848. }
  849. return this.wktFormat.writeGeometry(new MultiPolygon(coordSets), wktOpts);
  850. }
  851. getSelectedGridIds() {
  852. return this.getSelectedGrids().gridIds;
  853. }
  854. /**
  855. * 接口网格 geometry 转 Polygon(支持 MULTIPOINT / POLYGON)
  856. */
  857. gridGeometryToPolygon(geometryWkt) {
  858. if (!this.kmap || !geometryWkt) return null;
  859. const projection = this.kmap.map.getView().getProjection();
  860. let geom;
  861. try {
  862. geom = this.wktFormat.readGeometry(String(geometryWkt).trim(), {
  863. dataProjection: "EPSG:4326",
  864. featureProjection: projection,
  865. });
  866. } catch {
  867. return null;
  868. }
  869. const type = geom.getType();
  870. if (type === "Polygon") return geom;
  871. if (type === "MultiPolygon") {
  872. const polygons = geom.getPolygons();
  873. return polygons.length ? polygons[0] : null;
  874. }
  875. if (type !== "MultiPoint") return null;
  876. const coords = geom.getCoordinates();
  877. if (!coords || coords.length < 3) return null;
  878. const ring = coords.map((c) => [...c]);
  879. const first = ring[0];
  880. const last = ring[ring.length - 1];
  881. if (first[0] !== last[0] || first[1] !== last[1]) {
  882. ring.push([...first]);
  883. }
  884. return new Polygon([ring]);
  885. }
  886. /**
  887. * 渲染地形网格(generateGrid 接口返回)
  888. * @param {{ id: number, geometry: string, area_m2?: number }[]} gridItems
  889. */
  890. setTerrainGrids(gridItems) {
  891. if (!this.kmap || !this.gridLayer?.source) return;
  892. this.clearGridLayer();
  893. if (!Array.isArray(gridItems) || !gridItems.length) return;
  894. this.terrainGridItems = gridItems.map((item) => ({
  895. id: item.id,
  896. geometry: item.geometry,
  897. area_m2: item.area_m2,
  898. }));
  899. gridItems.forEach((item) => {
  900. const polygon = this.gridGeometryToPolygon(item?.geometry);
  901. if (!polygon) return;
  902. const feature = new Feature({ geometry: polygon });
  903. feature.set("gridId", item.id);
  904. feature.set("area_m2", item.area_m2);
  905. feature.set("selected", false);
  906. this.gridLayer.addFeature(feature);
  907. });
  908. this.bindGridClick();
  909. }
  910. fitGridView() {
  911. if (!this.kmap || !this.gridLayer?.source) return;
  912. const extent = this.gridLayer.source.getExtent();
  913. if (!extent || extent.some((v) => !Number.isFinite(v))) return;
  914. this.kmap.getView().fit(extent, { duration: 500, padding: [40, 40, 40, 40] });
  915. }
  916. getBoundaryWkt() {
  917. if (!this.boundaryGeometry || !this.kmap) return "";
  918. return this.wktFormat.writeGeometry(this.boundaryGeometry, {
  919. dataProjection: "EPSG:4326",
  920. featureProjection: this.kmap.map.getView().getProjection(),
  921. });
  922. }
  923. getDisplayAreaWkt() {
  924. if (!this.kmap?.polygonLayer?.source) return "";
  925. const projection = this.kmap.map.getView().getProjection();
  926. const geometryArr = [];
  927. this.kmap.polygonLayer.source.getFeatures().forEach((feature) => {
  928. const geometry = feature.getGeometry();
  929. if (!geometry) return;
  930. geometryArr.push(
  931. this.wktFormat.writeGeometry(geometry, {
  932. dataProjection: "EPSG:4326",
  933. featureProjection: projection,
  934. })
  935. );
  936. });
  937. return this.mergeGeometryWkts(geometryArr);
  938. }
  939. getTerrainGridItems() {
  940. if (this.terrainGridItems.length) {
  941. return this.terrainGridItems.map((item) => ({ ...item }));
  942. }
  943. if (!this.kmap || !this.gridLayer?.source) return [];
  944. const projection = this.kmap.map.getView().getProjection();
  945. return this.gridLayer.source.getFeatures().map((feature) => {
  946. const geometry = feature.getGeometry();
  947. return {
  948. id: feature.get("gridId"),
  949. geometry: geometry
  950. ? this.wktFormat.writeGeometry(geometry, {
  951. dataProjection: "EPSG:4326",
  952. featureProjection: projection,
  953. })
  954. : "",
  955. area_m2: feature.get("area_m2"),
  956. };
  957. });
  958. }
  959. restoreSelectedGrids(gridIds) {
  960. if (!this.gridLayer?.source || !Array.isArray(gridIds)) return;
  961. const idSet = new Set(gridIds.map((id) => String(id)));
  962. this.selectedGridIds.clear();
  963. this.gridLayer.source.getFeatures().forEach((feature) => {
  964. const gridId = feature.get("gridId");
  965. const selected = idSet.has(String(gridId));
  966. feature.set("selected", selected);
  967. if (selected) {
  968. this.selectedGridIds.add(gridId);
  969. }
  970. feature.changed();
  971. });
  972. this.gridLayer.layer.changed();
  973. }
  974. destroyMap() {
  975. this.unbindGridClick();
  976. this.clearAllLayers();
  977. this.centerMarkerLayer = null;
  978. if (this.kmap && typeof this.kmap.destroy === "function") {
  979. this.kmap.destroy();
  980. }
  981. this.kmap = null;
  982. this.regionDrawingActive = false;
  983. this.constrainedDrawing = false;
  984. this.constrainedDrawingReady = false;
  985. this.overlapConstraintReady = false;
  986. this.preventOverlapExisting = false;
  987. this.singleDraw = false;
  988. this.boundaryGeometry = null;
  989. this.onDrawOutsideBoundary = null;
  990. this.onDrawOverlap = null;
  991. this.onDrawLimit = null;
  992. this.selectedGridIds?.clear();
  993. }
  994. /**
  995. * 地图上全部已勾画地块:WKT 列表、每块亩数、合计亩数(亩换算与互动勾画页一致)
  996. */
  997. getAreaGeometry() {
  998. if (!this.kmap) {
  999. return { geometryArr: [], mianji: "0.00", parcels: [] };
  1000. }
  1001. const features = this.kmap.getLayerFeatures();
  1002. const format = new WKT();
  1003. const projection = this.kmap.map.getView().getProjection();
  1004. const geometryArr = [];
  1005. const parcels = [];
  1006. let totalMu = 0;
  1007. features.forEach((item) => {
  1008. const geometry = item.getGeometry();
  1009. if (!geometry) return;
  1010. const wkt = format.writeGeometry(geometry, {
  1011. dataProjection: "EPSG:4326",
  1012. featureProjection: projection,
  1013. });
  1014. geometryArr.push(wkt);
  1015. let geom = geometry.clone();
  1016. geom.transform(proj.get("EPSG:4326"), proj.get("EPSG:38572"));
  1017. let mu = getArea(geom);
  1018. mu = (mu + mu / 2) / 1000;
  1019. totalMu += mu;
  1020. parcels.push({ wkt, mianji: Number(mu.toFixed(2)) });
  1021. });
  1022. return {
  1023. geometryArr,
  1024. mianji: totalMu.toFixed(2),
  1025. parcels,
  1026. };
  1027. }
  1028. setAreaGeometry(geometryArr) {
  1029. this.clearLayer();
  1030. if (!this.kmap) return;
  1031. const format = new WKT();
  1032. const mapProjection = this.kmap.map.getView().getProjection();
  1033. geometryArr.forEach((item) => {
  1034. const geometry = format.readGeometry(item, {
  1035. dataProjection: "EPSG:4326",
  1036. featureProjection: mapProjection,
  1037. });
  1038. const feature = new Feature({ geometry });
  1039. if (!this.editable) {
  1040. feature.setStyle(this.createReadonlyPolygonStyle());
  1041. }
  1042. this.kmap.polygonLayer.source.addFeature(feature);
  1043. });
  1044. if (this.boundaryGeometry) {
  1045. this.fitBoundaryView();
  1046. } else {
  1047. this.fitView();
  1048. }
  1049. }
  1050. fitView(){
  1051. let extent = this.kmap.polygonLayer.source.getExtent()
  1052. // 地图自适应到区域可视范围
  1053. this.kmap.getView().fit(extent, { duration: 500, padding: [40, 40, 40, 40] });
  1054. }
  1055. }
  1056. export default MapManage;