|
@@ -2,163 +2,226 @@ import * as KMap from "@/utils/ol-map/KMap";
|
|
|
import * as util from "@/common/ol_common.js";
|
|
import * as util from "@/common/ol_common.js";
|
|
|
import config from "@/api/config.js";
|
|
import config from "@/api/config.js";
|
|
|
import { Vector as VectorSource } from "ol/source.js";
|
|
import { Vector as VectorSource } from "ol/source.js";
|
|
|
-import { newAreaFeature, newPoint } from "@/utils/map";
|
|
|
|
|
import Style from "ol/style/Style";
|
|
import Style from "ol/style/Style";
|
|
|
-import Icon from "ol/style/Icon";
|
|
|
|
|
import Text from "ol/style/Text";
|
|
import Text from "ol/style/Text";
|
|
|
import { Fill, Stroke } from "ol/style";
|
|
import { Fill, Stroke } from "ol/style";
|
|
|
-import { createEmpty, extend as extendExtent, isEmpty as isEmptyExtent } from "ol/extent";
|
|
|
|
|
-
|
|
|
|
|
-/** @param {string|string[]|undefined|null} wkt */
|
|
|
|
|
-function normalizePolygonWktList(wkt) {
|
|
|
|
|
- if (wkt == null) return [];
|
|
|
|
|
- if (typeof wkt === "string") {
|
|
|
|
|
- const s = wkt.trim();
|
|
|
|
|
- return s ? [s] : [];
|
|
|
|
|
- }
|
|
|
|
|
- if (Array.isArray(wkt)) {
|
|
|
|
|
- return wkt.map((x) => (typeof x === "string" ? x.trim() : "")).filter(Boolean);
|
|
|
|
|
- }
|
|
|
|
|
- return [];
|
|
|
|
|
|
|
+import { WKT } from "ol/format";
|
|
|
|
|
+import Feature from "ol/Feature";
|
|
|
|
|
+import {
|
|
|
|
|
+ createEmpty,
|
|
|
|
|
+ extend as extendExtent,
|
|
|
|
|
+ isEmpty as isEmptyExtent,
|
|
|
|
|
+ buffer as bufferExtent,
|
|
|
|
|
+ getWidth,
|
|
|
|
|
+ getHeight,
|
|
|
|
|
+} from "ol/extent";
|
|
|
|
|
+
|
|
|
|
|
+const WKT_FORMAT = new WKT();
|
|
|
|
|
+
|
|
|
|
|
+const RECORD_TYPE_STYLE = {
|
|
|
|
|
+ zone: { fill: "rgba(28, 158, 128, 0.45)", stroke: "#1c9e80" },
|
|
|
|
|
+ growth: { fill: "rgba(255, 120, 0, 0.5)", stroke: "#cc5500" },
|
|
|
|
|
+ pest: { fill: "rgba(224, 49, 49, 0.45)", stroke: "#e03131" },
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+/** 物候→管理分区,农事→长势异常,异常→病虫害异常 */
|
|
|
|
|
+const TAB_STYLE_TYPE = {
|
|
|
|
|
+ phenology: "zone",
|
|
|
|
|
+ farming: "growth",
|
|
|
|
|
+ abnormal: "pest",
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+export const RECORD_TAB_KEYS = ["phenology", "farming", "abnormal"];
|
|
|
|
|
+
|
|
|
|
|
+const DEFAULT_CENTER = "POINT(113.6142086995688 23.585836479509055)";
|
|
|
|
|
+
|
|
|
|
|
+function getItemPolygon(item) {
|
|
|
|
|
+ const wkt = item?.polygon ?? item?.geom ?? item?.geomWkt;
|
|
|
|
|
+ return typeof wkt === "string" ? wkt.trim() : "";
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function getRecordPointWkt(item) {
|
|
|
|
|
- if (!item || typeof item !== "object") return null;
|
|
|
|
|
- const candidates = [item.pointWkt, item.point_wkt, item.positionWkt, item.position_wkt];
|
|
|
|
|
- for (const v of candidates) {
|
|
|
|
|
- if (v && typeof v === "string" && v.trim().toUpperCase().startsWith("POINT")) {
|
|
|
|
|
- return v.trim();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+function readPolygonGeometry(wkt, projection) {
|
|
|
|
|
+ return WKT_FORMAT.readGeometry(wkt, {
|
|
|
|
|
+ dataProjection: "EPSG:4326",
|
|
|
|
|
+ featureProjection: projection,
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function createRecordFeature(wkt, item, projection, tabKey) {
|
|
|
|
|
+ const feature = new Feature({
|
|
|
|
|
+ geometry: readPolygonGeometry(wkt, projection),
|
|
|
|
|
+ });
|
|
|
|
|
+ feature.set("zone_name", item.zone_name);
|
|
|
|
|
+ feature.set("styleType", TAB_STYLE_TYPE[tabKey] || "zone");
|
|
|
|
|
+ return feature;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getUniqueExtents(features) {
|
|
|
|
|
+ const seen = new Set();
|
|
|
|
|
+ const extents = [];
|
|
|
|
|
+ features.forEach((f) => {
|
|
|
|
|
+ const e = f.getGeometry()?.getExtent();
|
|
|
|
|
+ if (!e || !Number.isFinite(e[0])) return;
|
|
|
|
|
+ const key = e.map((v) => v.toFixed(6)).join(",");
|
|
|
|
|
+ if (seen.has(key)) return;
|
|
|
|
|
+ seen.add(key);
|
|
|
|
|
+ extents.push(e);
|
|
|
|
|
+ });
|
|
|
|
|
+ return extents;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function resolveFitExtent(features) {
|
|
|
|
|
+ const extents = getUniqueExtents(features);
|
|
|
|
|
+ if (!extents.length) return null;
|
|
|
|
|
+
|
|
|
|
|
+ const fitOne = (e) => {
|
|
|
|
|
+ const span = Math.max(getWidth(e), getHeight(e), 0.00001);
|
|
|
|
|
+ return bufferExtent(e, span * 0.35);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (extents.length === 1) return fitOne(extents[0]);
|
|
|
|
|
+
|
|
|
|
|
+ const union = createEmpty();
|
|
|
|
|
+ extents.forEach((e) => extendExtent(union, e));
|
|
|
|
|
+ if (getWidth(union) > 0.15 || getHeight(union) > 0.15) {
|
|
|
|
|
+ return fitOne(extents[0]);
|
|
|
}
|
|
}
|
|
|
- const w = item.wkt;
|
|
|
|
|
- if (w && typeof w === "string" && w.trim().toUpperCase().startsWith("POINT")) {
|
|
|
|
|
- return w.trim();
|
|
|
|
|
|
|
+ return bufferExtent(union, Math.max(getWidth(union), getHeight(union), 0.00001) * 0.12);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function recordsToCenterPoint(records) {
|
|
|
|
|
+ const wkt = getItemPolygon(records?.[0]);
|
|
|
|
|
+ if (!wkt) return null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const c = util.wktCastGeom(wkt).getFirstCoordinate();
|
|
|
|
|
+ return `POINT(${c[0]} ${c[1]})`;
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ return null;
|
|
|
}
|
|
}
|
|
|
- return null;
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- *
|
|
|
|
|
- */
|
|
|
|
|
class FileMap {
|
|
class FileMap {
|
|
|
constructor() {
|
|
constructor() {
|
|
|
- /** @type {{ polygonWkt: string|string[]|undefined, pointItems: object[] } | null} */
|
|
|
|
|
- this._pendingFarmRecordOverlay = null;
|
|
|
|
|
|
|
+ this._pending = null;
|
|
|
|
|
+ this._fitTimer = null;
|
|
|
|
|
+ this._renderToken = 0;
|
|
|
const vectorStyle = new KMap.VectorStyle();
|
|
const vectorStyle = new KMap.VectorStyle();
|
|
|
- this.gardenPolygonLayer = new KMap.VectorLayer("gardenPolygonLayer", 999, {
|
|
|
|
|
- minZoom: 8,
|
|
|
|
|
- maxZoom: 22,
|
|
|
|
|
- source: new VectorSource({}),
|
|
|
|
|
- style: function () {
|
|
|
|
|
- return [vectorStyle.getPolygonStyle("rgba(24, 170, 139, 0.25)", "#18AA8B", 2)];
|
|
|
|
|
- },
|
|
|
|
|
- });
|
|
|
|
|
|
|
|
|
|
- this.recordPointLayer = new KMap.VectorLayer("fileRecordPointLayer", 1000, {
|
|
|
|
|
|
|
+ this.recordPolygonLayer = new KMap.VectorLayer("fileRecordPolygonLayer", 1000, {
|
|
|
minZoom: 8,
|
|
minZoom: 8,
|
|
|
maxZoom: 22,
|
|
maxZoom: 22,
|
|
|
source: new VectorSource({}),
|
|
source: new VectorSource({}),
|
|
|
style: (f) => {
|
|
style: (f) => {
|
|
|
- const pointIcon = new Style({
|
|
|
|
|
- image: new Icon({
|
|
|
|
|
- src: require("@/assets/img/home/garden-point.png"),
|
|
|
|
|
- scale: 0.5,
|
|
|
|
|
- anchor: [0.5, 1],
|
|
|
|
|
|
|
+ const colors = RECORD_TYPE_STYLE[f.get("styleType")] || RECORD_TYPE_STYLE.zone;
|
|
|
|
|
+ const polygonStyle = vectorStyle.getPolygonStyle(colors.fill, colors.stroke, 3);
|
|
|
|
|
+ const label = f.get("zone_name");
|
|
|
|
|
+ if (!label) return [polygonStyle];
|
|
|
|
|
+ return [
|
|
|
|
|
+ polygonStyle,
|
|
|
|
|
+ new Style({
|
|
|
|
|
+ text: new Text({
|
|
|
|
|
+ font: "12px sans-serif",
|
|
|
|
|
+ text: label,
|
|
|
|
|
+ fill: new Fill({ color: "#fff" }),
|
|
|
|
|
+ stroke: new Stroke({ color: "#000", width: 0.5 }),
|
|
|
|
|
+ }),
|
|
|
}),
|
|
}),
|
|
|
- });
|
|
|
|
|
- const label = f.get("labelText");
|
|
|
|
|
- const nameText = label
|
|
|
|
|
- ? new Style({
|
|
|
|
|
- text: new Text({
|
|
|
|
|
- font: "12px sans-serif",
|
|
|
|
|
- text: label,
|
|
|
|
|
- offsetY: -28,
|
|
|
|
|
- fill: new Fill({ color: "#fff" }),
|
|
|
|
|
- stroke: new Stroke({ color: "#000", width: 0.5 }),
|
|
|
|
|
- }),
|
|
|
|
|
- })
|
|
|
|
|
- : null;
|
|
|
|
|
- return nameText ? [pointIcon, nameText] : [pointIcon];
|
|
|
|
|
|
|
+ ];
|
|
|
},
|
|
},
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- initMap(location, target) {
|
|
|
|
|
- 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.gardenPolygonLayer.layer);
|
|
|
|
|
- this.kmap.addLayer(this.recordPointLayer.layer);
|
|
|
|
|
- if (this._pendingFarmRecordOverlay) {
|
|
|
|
|
- const p = this._pendingFarmRecordOverlay;
|
|
|
|
|
- this._pendingFarmRecordOverlay = null;
|
|
|
|
|
- this.setFarmRecordOverlay(p.polygonWkt, p.pointItems);
|
|
|
|
|
|
|
+ initMap(centerWkt, target) {
|
|
|
|
|
+ if (!target) return;
|
|
|
|
|
+
|
|
|
|
|
+ const center = util.wktCastGeom(centerWkt || DEFAULT_CENTER).getFirstCoordinate();
|
|
|
|
|
+
|
|
|
|
|
+ if (this.kmap?.map) {
|
|
|
|
|
+ this.kmap.map.setTarget(target);
|
|
|
|
|
+ this.kmap.map.updateSize();
|
|
|
|
|
+ if (this._pending) {
|
|
|
|
|
+ const pending = this._pending;
|
|
|
|
|
+ this._pending = null;
|
|
|
|
|
+ this.setRecordPolygons(pending.records, pending.tabKey);
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.kmap = new KMap.Map(target, 16, center[0], center[1], null, 8, 22);
|
|
|
|
|
+ this.kmap.addXYZLayer(config.base_img_url3 + "map/lby/{z}/{x}/{y}.png", { minZoom: 8, maxZoom: 22 }, 2);
|
|
|
|
|
+ this.kmap.addLayer(this.recordPolygonLayer.layer);
|
|
|
|
|
+
|
|
|
|
|
+ if (this._pending) {
|
|
|
|
|
+ const pending = this._pending;
|
|
|
|
|
+ this._pending = null;
|
|
|
|
|
+ this.setRecordPolygons(pending.records, pending.tabKey);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 农情档案:地块边界 + 记录点位(带 pointWkt / wkt 等)
|
|
|
|
|
- * @param {string|string[]} polygonWkt 地块 MULTIPOLYGON / POLYGON WKT,可为多条
|
|
|
|
|
- * @param {Array<object>} pointItems 当前 Tab 下列表项
|
|
|
|
|
- */
|
|
|
|
|
- setFarmRecordOverlay(polygonWkt, pointItems) {
|
|
|
|
|
|
|
+ setRecordPolygons(records, tabKey = "phenology") {
|
|
|
if (!this.kmap) {
|
|
if (!this.kmap) {
|
|
|
- this._pendingFarmRecordOverlay = { polygonWkt, pointItems };
|
|
|
|
|
|
|
+ this._pending = { records, tabKey };
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- this.gardenPolygonLayer.source.clear();
|
|
|
|
|
- this.recordPointLayer.source.clear();
|
|
|
|
|
|
|
+ if (this._fitTimer) {
|
|
|
|
|
+ clearTimeout(this._fitTimer);
|
|
|
|
|
+ this._fitTimer = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ const renderToken = ++this._renderToken;
|
|
|
|
|
|
|
|
- const wktList = normalizePolygonWktList(polygonWkt);
|
|
|
|
|
- wktList.forEach((w, idx) => {
|
|
|
|
|
- try {
|
|
|
|
|
- const f = newAreaFeature(
|
|
|
|
|
- { id: `farm-record-boundary-${idx}`, geomWkt: w },
|
|
|
|
|
- "geomWkt"
|
|
|
|
|
- );
|
|
|
|
|
- this.gardenPolygonLayer.source.addFeature(f);
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.warn("[FileMap] polygon WKT parse failed", e);
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ const projection = this.kmap.map.getView().getProjection();
|
|
|
|
|
+ const source = this.recordPolygonLayer.source;
|
|
|
|
|
+ source.clear(true);
|
|
|
|
|
+
|
|
|
|
|
+ const list = Array.isArray(records) ? records : [];
|
|
|
|
|
+ const seenWkt = new Set();
|
|
|
|
|
|
|
|
- const list = Array.isArray(pointItems) ? pointItems : [];
|
|
|
|
|
- list.forEach((item, idx) => {
|
|
|
|
|
- const ptWkt = getRecordPointWkt(item);
|
|
|
|
|
- if (!ptWkt) return;
|
|
|
|
|
|
|
+ list.forEach((item) => {
|
|
|
|
|
+ const wkt = getItemPolygon(item);
|
|
|
|
|
+ if (!wkt || seenWkt.has(wkt)) return;
|
|
|
|
|
+ seenWkt.add(wkt);
|
|
|
try {
|
|
try {
|
|
|
- const row = { ...item, id: item.id ?? `record-pt-${idx}`, pointWkt: ptWkt };
|
|
|
|
|
- const f = newPoint(row, "pointWkt");
|
|
|
|
|
- const labelText = [item.time, item.record].filter(Boolean).join(" ");
|
|
|
|
|
- f.set("labelText", labelText || String(item.record ?? ""));
|
|
|
|
|
- this.recordPointLayer.source.addFeature(f);
|
|
|
|
|
|
|
+ source.addFeature(createRecordFeature(wkt, item, projection, tabKey));
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
- console.warn("[FileMap] record point WKT parse failed", e);
|
|
|
|
|
|
|
+ console.warn("[FileMap] polygon parse failed", e);
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- this.fitView();
|
|
|
|
|
|
|
+ this.recordPolygonLayer.layer.changed();
|
|
|
|
|
+ source.changed();
|
|
|
|
|
+ this.fitView(renderToken);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 调整地图视图以适应地块与点位范围
|
|
|
|
|
- */
|
|
|
|
|
- fitView() {
|
|
|
|
|
- if (!this.kmap) return;
|
|
|
|
|
- let extent = createEmpty();
|
|
|
|
|
- const polyFeats = this.gardenPolygonLayer.source.getFeatures().length;
|
|
|
|
|
- const ptFeats = this.recordPointLayer.source.getFeatures().length;
|
|
|
|
|
- if (polyFeats) {
|
|
|
|
|
- extendExtent(extent, this.gardenPolygonLayer.source.getExtent());
|
|
|
|
|
- }
|
|
|
|
|
- if (ptFeats) {
|
|
|
|
|
- extendExtent(extent, this.recordPointLayer.source.getExtent());
|
|
|
|
|
|
|
+ fitView(renderToken) {
|
|
|
|
|
+ if (!this.kmap?.map) return;
|
|
|
|
|
+ if (renderToken != null && renderToken !== this._renderToken) return;
|
|
|
|
|
+
|
|
|
|
|
+ const map = this.kmap.map;
|
|
|
|
|
+ map.updateSize();
|
|
|
|
|
+
|
|
|
|
|
+ const features = this.recordPolygonLayer.source.getFeatures();
|
|
|
|
|
+ const extent = resolveFitExtent(features);
|
|
|
|
|
+ if (!extent || isEmptyExtent(extent)) return;
|
|
|
|
|
+
|
|
|
|
|
+ const size = map.getSize();
|
|
|
|
|
+ if (!size || size[0] < 4 || size[1] < 4) {
|
|
|
|
|
+ this._fitTimer = setTimeout(() => this.fitView(renderToken), 120);
|
|
|
|
|
+ return;
|
|
|
}
|
|
}
|
|
|
- if (!isEmptyExtent(extent)) {
|
|
|
|
|
- this.kmap.getView().fit(extent, { duration: 50, padding: [80, 80, 80, 80] });
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const view = this.kmap.getView();
|
|
|
|
|
+ view.cancelAnimations?.();
|
|
|
|
|
+
|
|
|
|
|
+ // Tab 切换时不用动画,避免连续 fit 互相打断导致视图停在上一 Tab 区域
|
|
|
|
|
+ view.fit(extent, {
|
|
|
|
|
+ size,
|
|
|
|
|
+ duration: 0,
|
|
|
|
|
+ padding: [100, 40, 40, 40],
|
|
|
|
|
+ maxZoom: 19,
|
|
|
|
|
+ });
|
|
|
|
|
+ if ((view.getZoom() ?? 0) < 16) {
|
|
|
|
|
+ view.setZoom(16);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|