import * as KMap from "@/utils/ol-map/KMap"; import * as util from "@/common/ol_common.js"; import config from "@/api/config.js"; import Style from "ol/style/Style"; import Icon from "ol/style/Icon"; import Fill from "ol/style/Fill"; import Stroke from "ol/style/Stroke"; import { Point, Polygon, MultiPolygon } from "ol/geom"; import Feature from "ol/Feature"; import DragPan from "ol/interaction/DragPan"; import MouseWheelZoom from "ol/interaction/MouseWheelZoom"; import PinchZoom from "ol/interaction/PinchZoom"; import PinchRotate from "ol/interaction/PinchRotate"; import DoubleClickZoom from "ol/interaction/DoubleClickZoom"; import KeyboardPan from "ol/interaction/KeyboardPan"; import KeyboardZoom from "ol/interaction/KeyboardZoom"; import DragRotateAndZoom from "ol/interaction/DragRotateAndZoom"; import Select from "ol/interaction/Select"; import { singleClick } from "ol/events/condition"; import { reactive } from "vue"; import WKT from "ol/format/WKT.js"; import GeoJSON from "ol/format/GeoJSON"; import * as proj from "ol/proj"; import { getArea } from "ol/sphere.js"; import * as turf from "@turf/turf"; const DEFAULT_ZONE_STYLE = { fill: "rgba(100, 0, 0, 0.45)", fillSelected: "rgba(100, 0, 0, 0.5)", stroke: "#E03131", }; const VIEWPORT_INTERACTION_TYPES = [ DragPan, MouseWheelZoom, PinchZoom, PinchRotate, DoubleClickZoom, KeyboardPan, KeyboardZoom, DragRotateAndZoom, ]; function setViewportInteractionsActive(olMap, active) { olMap.getInteractions().forEach((ix) => { if (VIEWPORT_INTERACTION_TYPES.some((T) => ix instanceof T)) { ix.setActive(active); } }); } export let mapLocation = reactive({ data: null, }); /** * @description 地图层对象 */ class MapManage { constructor() { let vectorStyle = new KMap.VectorStyle(); this.vectorStyle = vectorStyle; this.regionDrawingActive = false; this.clickPointLayer = new KMap.VectorLayer("clickPointLayer", 9999, { style: () => { return new Style({ image: new Icon({ src: require("@/assets/img/home/garden-point.png"), scale: 0.5, anchor: [0.5, 0.5], }), }); }, }); this.boundaryLayer = new KMap.VectorLayer("drawBoundaryLayer", 1050, { style: () => this.createBoundaryStyle(), }); this.boundaryGeometry = null; this.constrainedDrawing = false; this.constrainedDrawingReady = false; this.zoneStyle = { ...DEFAULT_ZONE_STYLE }; this.gridLayer = new KMap.VectorLayer("terrainGridLayer", 1100, { style: (feature) => { const selected = !!feature.get("selected"); return new Style({ fill: new Fill({ color: selected ? this.zoneStyle.fillSelected : "rgba(255, 255, 255, 0.01)", }), stroke: new Stroke({ color: selected ? this.zoneStyle.stroke : "#fff", width: selected ? 1.8 : 1.2, }), }); }, }); this.gridToggleSelect = null; this.selectedGridIds = new Set(); this.terrainGridItems = []; this.wktFormat = new WKT(); this.editable = true; } createReadonlyPolygonStyle() { return new Style({ fill: new Fill({ color: "rgba(124, 124, 124, 0.5)", }), stroke: new Stroke({ color: "rgba(255, 255, 255, 0.55)", width: 2, }), }); } createBoundaryStyle() { return new Style({ fill: new Fill({ color: "rgba(124, 124, 124, 0.12)", }), stroke: new Stroke({ color: "rgba(255, 255, 255, 0.85)", width: 2, lineDash: [8, 4], }), }); } createDrawnZoneStyle() { return new Style({ fill: new Fill({ color: this.zoneStyle.fill, }), stroke: new Stroke({ color: this.zoneStyle.stroke, width: 1.8, }), }); } initMap(location, target, options = {}) { const { editable = true, constrainedDrawing = false, onDrawOutsideBoundary, zoneStyle } = options; this.zoneStyle = zoneStyle ? { ...DEFAULT_ZONE_STYLE, ...zoneStyle } : { ...DEFAULT_ZONE_STYLE }; this.editable = editable; this.constrainedDrawing = constrainedDrawing; this.onDrawOutsideBoundary = typeof onDrawOutsideBoundary === "function" ? onDrawOutsideBoundary : null; this.constrainedDrawingReady = false; this.boundaryGeometry = null; let level = 16; let coordinate = util.wktCastGeom(location).getFirstCoordinate(); this.kmap = new KMap.Map(target, level, coordinate[0], coordinate[1], null, 8, 22); let xyz2 = config.base_img_url3 + "map/lby/{z}/{x}/{y}.png"; this.kmap.addXYZLayer(xyz2, { minZoom: 8, maxZoom: 22 }, 2); // this.kmap.addLayer(this.clickPointLayer.layer); this.kmap.addLayer(this.boundaryLayer.layer); this.kmap.addLayer(this.gridLayer.layer); if (this.editable) { this.kmap.initDraw(() => {}); this.kmap.modifyDraw(); this.setRegionDrawingActive(false); } else if (this.constrainedDrawing) { this.setupConstrainedDrawing(); this.setConstrainedDrawingActive(false); } } isCoordinateInsideBoundary(coordinate) { if (!this.boundaryGeometry || !coordinate || !this.kmap) return false; try { return this.boundaryGeometry.intersectsCoordinate(coordinate); } catch { return false; } } notifyDrawOutsideBoundary() { this.onDrawOutsideBoundary?.(); } getLastCoordinateFromGeometry(geometry) { if (!geometry || typeof geometry.getType !== "function") return null; const type = geometry.getType(); if (type === "Point") return geometry.getCoordinates(); if (type === "LineString") { const coords = geometry.getCoordinates(); return coords.length ? coords[coords.length - 1] : null; } if (type === "Polygon") { const ring = geometry.getCoordinates()[0]; return ring?.length ? ring[ring.length - 1] : null; } if (type === "MultiPolygon") { const polys = geometry.getCoordinates(); const ring = polys[polys.length - 1]?.[0]; return ring?.length ? ring[ring.length - 1] : null; } return null; } /** 区域外勾画:中止当前绘制并移除无效地块 */ rejectOutsideDraw(feature) { if (feature && this.kmap?.polygonLayer?.source) { this.kmap.polygonLayer.source.removeFeature(feature); } if (this.kmap?.draw && typeof this.kmap.draw.abortDrawing === "function") { this.kmap.draw.abortDrawing(); } this.notifyDrawOutsideBoundary(); } unbindSketchBoundaryConstraint(sketch, listener) { sketch?.getGeometry()?.un("change", listener); } bindSketchBoundaryConstraint(sketch) { const onSketchChange = () => { const geometry = sketch.getGeometry(); if (!geometry) return; const type = geometry.getType(); // 仅在手绘轨迹(线)阶段判断越界;面要素未完成时 intersect 会误判 if (type !== "LineString" && type !== "Point") return; const last = this.getLastCoordinateFromGeometry(geometry); if (last && !this.isCoordinateInsideBoundary(last)) { this.unbindSketchBoundaryConstraint(sketch, onSketchChange); this.rejectOutsideDraw(); } }; sketch.getGeometry()?.on("change", onSketchChange); const cleanup = () => this.unbindSketchBoundaryConstraint(sketch, onSketchChange); this.kmap.draw.once("drawend", cleanup); this.kmap.draw.once("drawabort", cleanup); } setupConstrainedDrawing() { if (!this.kmap || this.constrainedDrawingReady) return; this.constrainedDrawingReady = true; this.kmap.initDraw((e) => { if (!e.feature) return; if (!this.clipFeatureToBoundary(e.feature)) { if (this.kmap?.polygonLayer?.source) { this.kmap.polygonLayer.source.removeFeature(e.feature); } this.notifyDrawOutsideBoundary(); } }); this.kmap.draw.on("drawstart", (e) => { const coordinate = e.coordinate || this.getLastCoordinateFromGeometry(e.feature?.getGeometry()); if (!coordinate || !this.isCoordinateInsideBoundary(coordinate)) { this.rejectOutsideDraw(); return; } this.bindSketchBoundaryConstraint(e.feature); }); this.kmap.modifyDraw((e) => { e.features.forEach((feature) => { if (!this.clipFeatureToBoundary(feature)) { if (this.kmap?.polygonLayer?.source) { this.kmap.polygonLayer.source.removeFeature(feature); } this.notifyDrawOutsideBoundary(); } }); }); } setConstrainedDrawingActive(active) { if (!this.kmap) return; if (this.kmap.draw) { this.kmap.draw.setActive(active); } if (this.kmap.modify) { this.kmap.modify.setActive(active); } } enableConstrainedDrawing() { if (!this.boundaryGeometry) return; this.setupConstrainedDrawing(); this.setConstrainedDrawingActive(true); } intersectWithBoundary(geometry) { if (!this.boundaryGeometry || !geometry || !this.kmap) return null; const geoJson = new GeoJSON(); const projection = this.kmap.map.getView().getProjection(); const opts = { dataProjection: "EPSG:4326", featureProjection: projection, }; try { const drawGeo = geoJson.writeGeometryObject(geometry, opts); const boundaryGeo = geoJson.writeGeometryObject(this.boundaryGeometry, opts); const drawnFeature = turf.feature(drawGeo); const boundaryFeature = turf.feature(boundaryGeo); let result = null; try { result = turf.intersect(turf.featureCollection([drawnFeature, boundaryFeature])); } catch { result = turf.intersect(drawnFeature, boundaryFeature); } if (!result?.geometry) return null; return geoJson.readGeometry(result.geometry, opts); } catch { return null; } } clipFeatureToBoundary(feature) { if (!feature || !this.boundaryGeometry || !this.kmap?.polygonLayer?.source) return false; const geometry = feature.getGeometry(); if (!geometry) return false; const clipped = this.intersectWithBoundary(geometry); if (!clipped) { this.kmap.polygonLayer.source.removeFeature(feature); return false; } feature.setGeometry(clipped); feature.setStyle(this.createDrawnZoneStyle()); return true; } setBoundaryWkt(wkt) { if (!this.kmap || !this.boundaryLayer?.source || !wkt) return; this.boundaryLayer.source.clear(); const mapProjection = this.kmap.map.getView().getProjection(); const geometry = this.wktFormat.readGeometry(String(wkt).trim(), { dataProjection: "EPSG:4326", featureProjection: mapProjection, }); this.boundaryGeometry = geometry.clone(); const feature = new Feature({ geometry }); feature.set("isBoundary", true); this.boundaryLayer.addFeature(feature); this.fitBoundaryView(); } clearBoundaryLayer() { this.boundaryLayer?.source?.clear(); this.boundaryGeometry = null; } fitBoundaryView() { if (!this.kmap || !this.boundaryLayer?.source) return; const extent = this.boundaryLayer.source.getExtent(); if (!extent || extent.some((v) => !Number.isFinite(v))) return; this.kmap.getView().fit(extent, { duration: 500, padding: [40, 40, 40, 40] }); } setDrawnAreaGeometry(geometryArr) { if (!this.kmap?.polygonLayer?.source || !Array.isArray(geometryArr)) return; this.kmap.polygonLayer.source.clear(); const mapProjection = this.kmap.map.getView().getProjection(); geometryArr.forEach((item) => { try { const geometry = this.wktFormat.readGeometry(String(item).trim(), { dataProjection: "EPSG:4326", featureProjection: mapProjection, }); const feature = new Feature({ geometry }); this.clipFeatureToBoundary(feature); if (feature.getGeometry()) { this.kmap.polygonLayer.source.addFeature(feature); } } catch { /* 单条解析失败则跳过 */ } }); } /** * 是否允许平移/缩放、勾画与编辑;为 false 时同时隐藏中心点位图标 */ setRegionDrawingActive(active) { if (!this.kmap) return; this.regionDrawingActive = active; setViewportInteractionsActive(this.kmap.map, active); if (this.kmap.draw) { this.kmap.draw.setActive(active); } if (this.kmap.modify) { this.kmap.modify.setActive(active); } if (active) { const c = this.kmap.getView().getCenter(); this.setMapPoint(c); } else { // this.clickPointLayer.source.clear(); } } enableRegionDrawing() { this.setRegionDrawingActive(true); } enableMapInteraction() { if (!this.kmap) return; setViewportInteractionsActive(this.kmap.map, true); } /** * 根据中心点和亩数生成正方形 WKT * @param {number[]} center [lng, lat] * @param {number} mu 面积(亩) */ generateSquareWktByMu(center, mu = 60) { const lng = parseFloat(center[0]); const lat = parseFloat(center[1]); const halfSide = Math.sqrt(mu * 666.67) / 2; const latDelta = halfSide / 111000; const lngDelta = halfSide / (111000 * Math.cos((lat * Math.PI) / 180)); const ring = [ [lng - lngDelta, lat + latDelta], [lng + lngDelta, lat + latDelta], [lng + lngDelta, lat - latDelta], [lng - lngDelta, lat - latDelta], [lng - lngDelta, lat + latDelta], ]; const coordinates = ring.map((point) => `${point[0]} ${point[1]}`).join(", "); return `MULTIPOLYGON (((${coordinates})))`; } /** * 以当前地图中心生成指定亩数的正方形区域 * @param {number} mu 面积(亩) * @returns {string|null} WKT */ setDefaultSquareAtCenter(mu = 60) { if (!this.kmap) return null; const center = this.kmap.getView().getCenter(); const wkt = this.generateSquareWktByMu(center, mu); this.setBoundaryWkt(wkt); this.setMapPoint(center); this.enableConstrainedDrawing(); return wkt; } setCenterAndSquare(center, mu = 60) { if (!this.kmap) return null; this.kmap.getView().animate({ center, zoom: 16, duration: 0, }); this.setMapPoint(center); const wkt = this.generateSquareWktByMu(center, mu); this.setBoundaryWkt(wkt); this.enableConstrainedDrawing(); return wkt; } setMapPoint(coordinate) { // this.clickPointLayer.source.clear(); let point = new Feature(new Point(coordinate)); // this.clickPointLayer.addFeature(point); } setMapPosition(center) { this.kmap.getView().animate({ center, zoom: 16, duration: 0, }); if (this.regionDrawingActive) { this.setMapPoint(center); } } clearLayer() { if (!this.kmap?.polygonLayer?.source) return; if (this.kmap.draw && typeof this.kmap.draw.abortDrawing === "function") { this.kmap.draw.abortDrawing(); } this.kmap.polygonLayer.source.clear(); this.clearGridLayer(); } clearAllLayers() { this.clearLayer(); this.clearBoundaryLayer(); } clearGridLayer() { this.unbindGridClick(); this.gridLayer?.source?.clear(); this.selectedGridIds?.clear(); this.terrainGridItems = []; } unbindGridClick() { if (this.gridToggleSelect && this.kmap?.map) { this.kmap.map.removeInteraction(this.gridToggleSelect); } this.gridToggleSelect = null; } bindGridClick() { if (!this.kmap?.map || !this.gridLayer?.layer) return; this.unbindGridClick(); const selectedStyle = new Style({ fill: new Fill({ color: this.zoneStyle.fillSelected, }), stroke: new Stroke({ color: this.zoneStyle.stroke, width: 1.8, }), }); this.gridToggleSelect = new Select({ condition: singleClick, toggleCondition: singleClick, layers: [this.gridLayer.layer], multi: true, hitTolerance: 8, style: selectedStyle, }); this.gridToggleSelect.on("select", (e) => { e.selected.forEach((feature) => { feature.set("selected", true); this.selectedGridIds.add(feature.get("gridId")); feature.changed(); }); e.deselected.forEach((feature) => { feature.set("selected", false); this.selectedGridIds.delete(feature.get("gridId")); feature.changed(); }); this.gridLayer.layer.changed(); }); this.kmap.map.addInteraction(this.gridToggleSelect); } getSelectedGrids() { const empty = { gridIds: [], geometryArr: [], parcels: [], mianji: "0.00", mergedGeometry: "", }; if (!this.kmap || !this.gridLayer?.source) return empty; const projection = this.kmap.map.getView().getProjection(); const gridIds = []; const geometryArr = []; const parcels = []; let totalMu = 0; this.gridLayer.source.getFeatures().forEach((feature) => { if (!feature.get("selected")) return; const geometry = feature.getGeometry(); if (!geometry) return; const gridId = feature.get("gridId"); gridIds.push(gridId); const wkt = this.wktFormat.writeGeometry(geometry, { dataProjection: "EPSG:4326", featureProjection: projection, }); geometryArr.push(wkt); let geom = geometry.clone(); geom.transform(proj.get("EPSG:4326"), proj.get("EPSG:38572")); let mu = getArea(geom); mu = (mu + mu / 2) / 1000; totalMu += mu; parcels.push({ gridId, wkt, mianji: Number(mu.toFixed(2)) }); }); return { gridIds, geometryArr, parcels, mianji: totalMu.toFixed(2), mergedGeometry: this.mergeGeometryWkts(geometryArr), }; } polygonCoordSetsFromGeometry(geometry) { if (!geometry || typeof geometry.getType !== "function") return []; const type = geometry.getType(); if (type === "Polygon") return [geometry.getCoordinates()]; if (type === "MultiPolygon") return geometry.getCoordinates(); return []; } /** * 将多个 WKT 面合并为一个(单块返回 POLYGON,多块返回 MULTIPOLYGON) * @param {string[]} wktArr * @returns {string} */ mergeGeometryWkts(wktArr) { if (!Array.isArray(wktArr) || wktArr.length === 0) return ""; const trimmed = wktArr .map((item) => String(item).trim()) .filter((item) => item.length > 10); if (trimmed.length === 0) return ""; if (trimmed.length === 1) return trimmed[0]; const wktOpts = { dataProjection: "EPSG:4326", featureProjection: "EPSG:4326", }; const coordSets = []; trimmed.forEach((wkt) => { try { const geometry = this.wktFormat.readGeometry(wkt, wktOpts); coordSets.push(...this.polygonCoordSetsFromGeometry(geometry)); } catch { /* 单条解析失败则跳过 */ } }); if (coordSets.length === 0) return trimmed[0]; if (coordSets.length === 1) { return this.wktFormat.writeGeometry(new Polygon(coordSets[0]), wktOpts); } return this.wktFormat.writeGeometry(new MultiPolygon(coordSets), wktOpts); } getSelectedGridIds() { return this.getSelectedGrids().gridIds; } /** * 接口网格 geometry 转 Polygon(支持 MULTIPOINT / POLYGON) */ gridGeometryToPolygon(geometryWkt) { if (!this.kmap || !geometryWkt) return null; const projection = this.kmap.map.getView().getProjection(); let geom; try { geom = this.wktFormat.readGeometry(String(geometryWkt).trim(), { dataProjection: "EPSG:4326", featureProjection: projection, }); } catch { return null; } const type = geom.getType(); if (type === "Polygon") return geom; if (type === "MultiPolygon") { const polygons = geom.getPolygons(); return polygons.length ? polygons[0] : null; } if (type !== "MultiPoint") return null; const coords = geom.getCoordinates(); if (!coords || coords.length < 3) return null; const ring = coords.map((c) => [...c]); const first = ring[0]; const last = ring[ring.length - 1]; if (first[0] !== last[0] || first[1] !== last[1]) { ring.push([...first]); } return new Polygon([ring]); } /** * 渲染地形网格(generateGrid 接口返回) * @param {{ id: number, geometry: string, area_m2?: number }[]} gridItems */ setTerrainGrids(gridItems) { if (!this.kmap || !this.gridLayer?.source) return; this.clearGridLayer(); if (!Array.isArray(gridItems) || !gridItems.length) return; this.terrainGridItems = gridItems.map((item) => ({ id: item.id, geometry: item.geometry, area_m2: item.area_m2, })); gridItems.forEach((item) => { const polygon = this.gridGeometryToPolygon(item?.geometry); if (!polygon) return; const feature = new Feature({ geometry: polygon }); feature.set("gridId", item.id); feature.set("area_m2", item.area_m2); feature.set("selected", false); this.gridLayer.addFeature(feature); }); this.bindGridClick(); } fitGridView() { if (!this.kmap || !this.gridLayer?.source) return; const extent = this.gridLayer.source.getExtent(); if (!extent || extent.some((v) => !Number.isFinite(v))) return; this.kmap.getView().fit(extent, { duration: 500, padding: [40, 40, 40, 40] }); } getBoundaryWkt() { if (!this.boundaryGeometry || !this.kmap) return ""; return this.wktFormat.writeGeometry(this.boundaryGeometry, { dataProjection: "EPSG:4326", featureProjection: this.kmap.map.getView().getProjection(), }); } getDisplayAreaWkt() { if (!this.kmap?.polygonLayer?.source) return ""; const projection = this.kmap.map.getView().getProjection(); const geometryArr = []; this.kmap.polygonLayer.source.getFeatures().forEach((feature) => { const geometry = feature.getGeometry(); if (!geometry) return; geometryArr.push( this.wktFormat.writeGeometry(geometry, { dataProjection: "EPSG:4326", featureProjection: projection, }) ); }); return this.mergeGeometryWkts(geometryArr); } getTerrainGridItems() { if (this.terrainGridItems.length) { return this.terrainGridItems.map((item) => ({ ...item })); } if (!this.kmap || !this.gridLayer?.source) return []; const projection = this.kmap.map.getView().getProjection(); return this.gridLayer.source.getFeatures().map((feature) => { const geometry = feature.getGeometry(); return { id: feature.get("gridId"), geometry: geometry ? this.wktFormat.writeGeometry(geometry, { dataProjection: "EPSG:4326", featureProjection: projection, }) : "", area_m2: feature.get("area_m2"), }; }); } restoreSelectedGrids(gridIds) { if (!this.gridLayer?.source || !Array.isArray(gridIds)) return; const idSet = new Set(gridIds.map((id) => String(id))); this.selectedGridIds.clear(); this.gridLayer.source.getFeatures().forEach((feature) => { const gridId = feature.get("gridId"); const selected = idSet.has(String(gridId)); feature.set("selected", selected); if (selected) { this.selectedGridIds.add(gridId); } feature.changed(); }); this.gridLayer.layer.changed(); } destroyMap() { this.unbindGridClick(); this.clearAllLayers(); if (this.kmap && typeof this.kmap.destroy === "function") { this.kmap.destroy(); } this.kmap = null; this.regionDrawingActive = false; this.constrainedDrawing = false; this.constrainedDrawingReady = false; this.boundaryGeometry = null; this.onDrawOutsideBoundary = null; this.selectedGridIds?.clear(); } /** * 地图上全部已勾画地块:WKT 列表、每块亩数、合计亩数(亩换算与互动勾画页一致) */ getAreaGeometry() { if (!this.kmap) { return { geometryArr: [], mianji: "0.00", parcels: [] }; } const features = this.kmap.getLayerFeatures(); const format = new WKT(); const projection = this.kmap.map.getView().getProjection(); const geometryArr = []; const parcels = []; let totalMu = 0; features.forEach((item) => { const geometry = item.getGeometry(); if (!geometry) return; const wkt = format.writeGeometry(geometry, { dataProjection: "EPSG:4326", featureProjection: projection, }); geometryArr.push(wkt); let geom = geometry.clone(); geom.transform(proj.get("EPSG:4326"), proj.get("EPSG:38572")); let mu = getArea(geom); mu = (mu + mu / 2) / 1000; totalMu += mu; parcels.push({ wkt, mianji: Number(mu.toFixed(2)) }); }); return { geometryArr, mianji: totalMu.toFixed(2), parcels, }; } setAreaGeometry(geometryArr) { this.clearLayer(); if (!this.kmap) return; const format = new WKT(); const mapProjection = this.kmap.map.getView().getProjection(); geometryArr.forEach((item) => { const geometry = format.readGeometry(item, { dataProjection: "EPSG:4326", featureProjection: mapProjection, }); const feature = new Feature({ geometry }); if (!this.editable) { feature.setStyle(this.createReadonlyPolygonStyle()); } this.kmap.polygonLayer.source.addFeature(feature); }); if (this.boundaryGeometry) { this.fitBoundaryView(); } else { this.fitView(); } } fitView(){ let extent = this.kmap.polygonLayer.source.getExtent() // 地图自适应到区域可视范围 this.kmap.getView().fit(extent, { duration: 500, padding: [40, 40, 40, 40] }); } } export default MapManage;