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, Stroke, Text } from "ol/style.js"; import { Point } from 'ol/geom'; import Feature from "ol/Feature"; import * as proj from "ol/proj"; import { getArea } from 'ol/sphere.js'; import WKT from "ol/format/WKT.js"; import proj4 from "proj4" import { register } from "ol/proj/proj4"; proj4.defs("EPSG:38572", "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs +type=crs"); register(proj4); /** * @description 地图层对象 */ class DrawRegionMap { constructor() { let that = this; let vectorStyle = new KMap.VectorStyle(); this.vectorStyle = vectorStyle; // 是否允许编辑(勾画页 true,小弹窗回显页 false) this.editable = true; // 位置图标 this.clickPointLayer = new KMap.VectorLayer("clickPointLayer", 9999, { style: () => { return new Style({ image: new Icon({ // src: require("@/assets/img/home/garden-point.png"), src: require("@/assets/img/map/map_point.png"), scale: 0.5, // anchor: [0.5, 0.5], }), }); }, }); // 只读状态区域图层(展示多 polygon,用于“已解决 / 未解决”等状态) // 层级需低于默认可编辑 polygonLayer(1000),避免标签遮挡当前编辑地块 this.staticRegionLayer = new KMap.VectorLayer("staticRegionLayer", 900, { style: (f) => { const displayMode = f.get("displayMode"); // 品种勾画页:已勾画的其他品种仅做只读灰色展示,并显示品种名 if (displayMode === "readonlyVariety") { // 品种区只读展示:统一使用品种区视觉样式 return new Style({ fill: new Fill({ color: "rgba(0, 57, 44, 0.5)" }), stroke: new Stroke({ color: "#18AA8B", width: 1.5 }), text: new Text({ text: f.get("label") || "", font: "12px sans-serif", fill: new Fill({ color: "#ffffff" }), backgroundFill: new Fill({ color: "rgba(0, 57, 44, 0.85)" }), padding: [2, 6, 2, 6], }), }); } const status = f.get("status"); // 'resolved' | 'unresolved' const reproductiveName = f.get("reproductiveName") || ""; const isResolved = status === "resolved"; const unresolvedBlueFill = f.get("unresolvedBlueFill") === true; // 已解决:深灰填充,浅白描边;未解决:默认深灰半透明 + 蓝描边;unresolvedBlueFill 时浅蓝填充(互动列表等) const fillColor = isResolved ? "rgba(0, 0, 0, 0.6)" : unresolvedBlueFill ? "rgba(33, 153, 248, 0.35)" : "rgba(0, 0, 0, 0.5)"; const strokeColor = isResolved ? "#7C7C7C" : "#2199F8"; const text = new Text({ text: reproductiveName ? `${reproductiveName} ${status === "resolved" ? "已解决" : "未解决"}` : status === "resolved" ? "已解决" : "未解决", font: "12px sans-serif", fill: new Fill({ color: "#ffffff" }), backgroundFill: new Fill({ color: isResolved ? "#949494" : "#2199F8" }), padding: [1, 5, 1, 5], }); const style = new Style({ fill: new Fill({ color: fillColor }), stroke: new Stroke({ color: strokeColor, width: 1 }), text: text, }) const text2 = new Style({ text: new Text({ text: `发现时间:${f.get("updatedTime")}`, font: "12px sans-serif", offsetY: -24, fill: new Fill({ color: "#ffffff" }), backgroundFill: new Fill({ color: isResolved ? "rgba(171, 171, 171, 0.4)" : "rgba(33, 153, 248, 0.6)" }), padding: [1, 5, 1, 5], }), }); return [style, text2]; }, }); } /** * 初始化地图 * @param {string} location WKT 点位 * @param {HTMLElement|string} target 地图容器 * @param {boolean} editable 是否允许绘制/编辑地块 * @param {boolean} movable 是否允许拖动/缩放地图 * @param {boolean} showPoint 是否显示初始点位图标 * @param {Function} onDrawEnd 绘制闭环回调(drawend),可选 */ initMap(location, target, editable = true, movable = true, showPoint = true, onDrawEnd) { let level = 16; let coordinate = util.wktCastGeom(location).getFirstCoordinate(); this.kmap = new KMap.Map(target, level, coordinate[0], coordinate[1], null, 8, 22); // 记录当前地图是否可编辑,供样式控制使用 this.editable = editable; 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.staticRegionLayer.layer); // 根据 showPoint 决定是否在初始化时落下点位图标 if (showPoint) { this.setMapPoint(coordinate); } else { this.clickPointLayer.source.clear(); } // 仅在 editable 为 true 时开启绘制/编辑能力(用于勾画页面) if (editable) { this.kmap.initDraw((e) => { if (typeof onDrawEnd === "function") { try { onDrawEnd(e); } catch (_) { // 回调失败不影响地图继续使用 } } }); this.kmap.startDraw() this.kmap.modifyDraw() } // movable 为 false 时,禁用地图拖动、缩放等交互(用于小弹窗只看不动) if (!movable && this.kmap && this.kmap.setStates) { this.kmap.setStates({ DoubleClickZoom: false, DragAndDrop: false, MouseWheelZoom: false, }); } } /** * 回显地块 * @param {string[]} geometryArr 多边形 WKT 数组 * @param {boolean} needFitView 是否自动缩放视图 * @param {string|number} areaText 显示的面积(单位:亩),可选 * @param {{ fill?: string, stroke?: string }} [readonlyAreaStyle] 只读模式下覆盖填充/描边色;不传则仍用 Map.drawStyleColors 或默认 * @param {{ badgeText: string, discoveryDate: string, badgeBackground?: string }} [growthOverlay] 只读模式下异常区标签(配色与勾画页一致) * @param {string} [polygonCenterLabel] 只读模式下覆盖多边形中心文案(原样展示,不加「亩」);用于品种查看态显示品种名等 */ setAreaGeometry(geometryArr, needFitView = false, areaText, readonlyAreaStyle, growthOverlay, polygonCenterLabel) { // 兜底保护:geometryArr 可能为 undefined/null 或空数组 if (!Array.isArray(geometryArr) || geometryArr.length === 0) return; // 地图实例或图层尚未初始化时也直接返回,避免报错 if (!this.kmap || !this.kmap.polygonLayer || !this.kmap.polygonLayer.source) return; let that = this; geometryArr.map(item => { // 不使用 setLayerWkt,而是手动添加要素,避免自动缩放视图 const format = new WKT() const mapProjection = that.kmap.map.getView().getProjection() let geometry = format.readGeometry(item, { dataProjection: 'EPSG:4326', featureProjection: mapProjection }) let f = new Feature({ geometry: geometry }) // 只读模式下,为多边形单独设置样式:仅填充+边框 + 面积文本,不显示可拖动的顶点小圆点 if (!this.editable) { // 查看模式下单块区域展示:优先 readonlyAreaStyle,其次 Map.drawStyleColors,再默认 const fillColor = readonlyAreaStyle?.fill ?? KMap.Map?.drawStyleColors?.fill ?? "rgba(0, 57, 44, 0.5)"; const strokeColor = readonlyAreaStyle?.stroke ?? KMap.Map?.drawStyleColors?.stroke ?? "#18AA8B"; const styles = [ new Style({ fill: new Fill({ color: fillColor, }), stroke: new Stroke({ color: strokeColor, width: 2, }), }), ]; // 中心文本:优先 polygonCenterLabel(品种查看态显示名称);否则按面积规则 const trimmedCenterLabel = typeof polygonCenterLabel === "string" && polygonCenterLabel.trim() !== "" ? polygonCenterLabel.trim() : ""; // 面积文本显示规则: // 1) 传空字符串:不显示 // 2) 传了值:优先显示传入值 // 3) 未传:按当前地块实时计算亩数 const isExplicitEmptyText = typeof areaText === "string" && areaText.trim() === ""; const hasProvidedAreaText = areaText !== undefined && areaText !== null; let textValue = ""; if (trimmedCenterLabel) { textValue = trimmedCenterLabel; } else if (!isExplicitEmptyText) { if (hasProvidedAreaText) { textValue = `${areaText}亩`; } else { try { let geom = geometry.clone(); geom.transform(proj.get("EPSG:4326"), proj.get("EPSG:38572")); let areaItem = getArea(geom); areaItem = (areaItem + areaItem / 2) / 1000; textValue = `${Number(areaItem).toFixed(2)}亩`; } catch (_) { textValue = ""; } } } const hasGrowth = growthOverlay && growthOverlay.badgeText && growthOverlay.discoveryDate; if (textValue) { // 品种名与 staticRegionLayer.readonlyVariety 标签视觉一致(12px + 深绿底) const isVarietyNameLabel = !!trimmedCenterLabel; styles.push( new Style({ text: new Text({ text: textValue, font: isVarietyNameLabel ? "12px sans-serif" : "15px sans-serif", fill: new Fill({ color: "#ffffff" }), ...(isVarietyNameLabel ? { backgroundFill: new Fill({ color: "rgba(0, 57, 44, 0.85)", }), padding: [2, 6, 2, 6], } : {}), offsetY: hasGrowth ? 14 : 0, }), }) ); } if (hasGrowth) { styles.push( new Style({ text: new Text({ text: growthOverlay.badgeText, font: "bold 13px sans-serif", fill: new Fill({ color: "#ffffff" }), backgroundFill: new Fill({ color: growthOverlay.badgeBackground || "#FF7F00", }), padding: [4, 10, 4, 10], offsetY: -40, }), }), new Style({ text: new Text({ text: `发现时间:${growthOverlay.discoveryDate}`, font: "12px sans-serif", fill: new Fill({ color: "#ffffff" }), offsetY: -16, }), }) ); } f.setStyle(styles); } that.kmap.polygonLayer.source.addFeature(f) }) // 根据参数决定是否需要自适应地块范围 if (needFitView) { this.fitView() } } fitView() { if (!this.kmap?.polygonLayer?.source) return; const extent = this.kmap.polygonLayer.source.getExtent(); if ( !extent || !isFinite(extent[0]) || !isFinite(extent[1]) || !isFinite(extent[2]) || !isFinite(extent[3]) ) { return; } this.kmap.getView().fit(extent, { duration: 500, padding: [10, 10, 10, 10] }); } clearLayer() { // this.kmap.removeLayer(this.clickPointLayer.layer) if (this.kmap && this.kmap.polygonLayer) { this.kmap.polygonLayer.source.clear(); } if (this.staticRegionLayer && this.staticRegionLayer.source) { this.staticRegionLayer.source.clear(); } } /** 取消当前未完成的勾画(草图在 Draw 的 overlay 上,仅 clear 多边形图层去不掉) */ abortOngoingDrawSketch() { if (!this.kmap || !this.kmap.draw || typeof this.kmap.draw.abortDrawing !== "function") return; this.kmap.draw.abortDrawing(); } /** * 销毁地图实例(用于从编辑态切换到仅查看时重新初始化) */ destroyMap() { this.clearLayer(); if (this.kmap && typeof this.kmap.destroy === "function") { this.kmap.destroy(); } this.kmap = null; } getAreaGeometry() { const features = this.kmap.getLayerFeatures() console.log(features, 'features'); let geometryArr = [] let area = 0 const format = new WKT() // 获取图层上的Polygon,转成WKT用于回显 features.forEach(item => { console.log(item, 'item'); // 使用 writeGeometry 而不是 writeFeature,因为 setLayerWkt 期望的是几何体的 WKT const geometry = item.getGeometry() geometryArr.push(format.writeGeometry(geometry, { dataProjection: 'EPSG:4326', featureProjection: this.kmap.map.getView().getProjection() })) let geom = geometry.clone() geom.transform(proj.get("EPSG:4326"), proj.get("EPSG:38572")) let areaItem = getArea(geom) areaItem = (areaItem + areaItem / 2) / 1000; area += areaItem }) return { geometryArr, mianji: area.toFixed(2) } // 修改为 mianji 字段,与创建页面保持一致 } setMapPosition(center) { this.kmap.getView().animate({ center, zoom: 17, duration: 500, }); this.setMapPoint(center) } setMapPoint(coordinate) { this.clickPointLayer.source.clear() let point = new Feature(new Point(coordinate)) this.clickPointLayer.addFeature(point) } // 删除当前地块(删除最新绘制的一个地块) deleteCurrentPolygon() { if (!this.kmap || !this.kmap.polygonLayer) return; const features = this.kmap.polygonLayer.source.getFeatures(); if (features && features.length > 0) { const lastFeature = features[features.length - 1]; this.kmap.polygonLayer.source.removeFeature(lastFeature); } } /** * 设置只读状态区域图层(多个 polygon,不可编辑) * @param {Array<{ geometry: string, status?: 'resolved' | 'unresolved', label?: string, displayMode?: string, unresolvedBlueFill?: boolean }>} regions * * 使用示例: * drawRegionMap.setStatusRegions([ * { geometry: 'MULTIPOLYGON(((...)))', status: 'resolved' }, * { geometry: 'MULTIPOLYGON(((...)))', status: 'unresolved' }, * ]); */ setStatusRegions(regions) { if (!this.kmap || !this.staticRegionLayer || !this.staticRegionLayer.source) return; // 仅操作只读图层,不影响当前地图的绘制 / 编辑状态 this.staticRegionLayer.source.clear(); if (!Array.isArray(regions) || regions.length === 0) return; const format = new WKT(); const mapProjection = this.kmap.map.getView().getProjection(); regions.forEach((region) => { if (!region || !region.geometry) return; try { const geometry = format.readGeometry(region.geometry, { dataProjection: "EPSG:4326", featureProjection: mapProjection, }); const feature = new Feature({ geometry }); feature.set("reproductiveName", region.reproductiveName); feature.set("status", region.status || "unresolved"); feature.set("updatedTime", region.updatedTime); feature.set("label", region.label || ""); feature.set("displayMode", region.displayMode || ""); feature.set("unresolvedBlueFill", region.unresolvedBlueFill === true); this.staticRegionLayer.addFeature(feature); } catch (e) { // 单个区域解析失败时忽略 } }); } /** * 视图自适应到「只读区域图层 + 可编辑多边形图层」的联合范围 * 适用于同时存在接口返回区域和本地勾画区域时,保证都能出现在视野内 */ fitAllRegions() { if (!this.kmap) return; const extents = []; // 只读状态区域图层范围 if (this.staticRegionLayer && this.staticRegionLayer.source) { const features = this.staticRegionLayer.source.getFeatures(); if (features && features.length > 0) { extents.push(this.staticRegionLayer.source.getExtent()); } } // 可编辑 polygon 图层范围 if (this.kmap.polygonLayer && this.kmap.polygonLayer.source) { const features = this.kmap.polygonLayer.source.getFeatures(); if (features && features.length > 0) { extents.push(this.kmap.polygonLayer.source.getExtent()); } } if (extents.length === 0) return; // 计算所有范围的并集 [minX, minY, maxX, maxY] const merged = extents.reduce( (acc, cur) => { if (!acc) return cur.slice(); return [ Math.min(acc[0], cur[0]), Math.min(acc[1], cur[1]), Math.max(acc[2], cur[2]), Math.max(acc[3], cur[3]), ]; }, null ); if (merged) { this.kmap.getView().fit(merged, { duration: 500, padding: [100, 100, 100, 100] }); } } } export default DrawRegionMap;