mapManage.js 35 KB

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