| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497 |
- import * as KMap from "@/utils/ol-map/KMap";
- import * as util from "@/common/ol_common.js";
- import config from "@/api/config.js";
- import StaticImgLayer from "@/utils/ol-map/StaticImgLayer";
- import { Vector as VectorSource } from "ol/source.js";
- import Style from "ol/style/Style";
- import Text from "ol/style/Text";
- import Icon from "ol/style/Icon";
- import { Fill, Stroke } from "ol/style";
- import { WKT } from "ol/format";
- import Feature from "ol/Feature";
- import Point from "ol/geom/Point";
- import {
- createEmpty,
- extend as extendExtent,
- isEmpty as isEmptyExtent,
- buffer as bufferExtent,
- getWidth,
- getHeight,
- } from "ol/extent";
- /** PNG 农场影像底图:extent 为 [minLon, minLat, maxLon, maxLat],仅以下农场有数据 */
- const FARM_BASE_IMAGE_LAYERS = [
- {
- name: "东莞市潢涌村水稻基地",
- farmId: 320,
- url: "https://birdseye-img.sysuimars.com/platform-rs/farm_320_20260314.png",
- extent: [113.48318638854593, 22.90042586164938, 113.98019565717107, 23.36008435819883],
- },
- {
- name: "莞荔园",
- farmId: 319,
- url: "https://birdseye-img.sysuimars.com/platform-rs/farm_319_20260314.png",
- extent: [113.49887532172826, 22.74031995946140, 113.99520269126261, 23.19988233865987],
- },
- ];
- const FARM_BASE_IMAGE_FARM_IDS = new Set(FARM_BASE_IMAGE_LAYERS.map((item) => item.farmId));
- export function hasFarmBaseImage(farmId) {
- if (farmId == null || farmId === "") return false;
- return FARM_BASE_IMAGE_FARM_IDS.has(Number(farmId));
- }
- 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" },
- album: { fill: "rgba(255, 255, 255, 0.28)", stroke: "#ffffff" },
- };
- /** 物候→管理分区,农事→管理分区,异常→病虫害异常,相册→白色分区 */
- const TAB_STYLE_TYPE = {
- phenology: "zone",
- farming: "zone",
- abnormal: "pest",
- album: "album",
- };
- export const RECORD_TAB_KEYS = ["phenology", "farming", "abnormal"];
- const DEFAULT_CENTER = "POINT(113.6142086995688 23.585836479509055)";
- function roundRectPath(ctx, x, y, width, height, radius) {
- const r = Math.min(radius, width / 2, height / 2);
- ctx.beginPath();
- ctx.moveTo(x + r, y);
- ctx.arcTo(x + width, y, x + width, y + height, r);
- ctx.arcTo(x + width, y + height, x, y + height, r);
- ctx.arcTo(x, y + height, x, y, r);
- ctx.arcTo(x, y, x + width, y, r);
- ctx.closePath();
- }
- function createHiDpiCanvas(cssWidth, cssHeight) {
- const dpr = window.devicePixelRatio || 1;
- const canvas = document.createElement("canvas");
- canvas.width = Math.ceil(cssWidth * dpr);
- canvas.height = Math.ceil(cssHeight * dpr);
- const ctx = canvas.getContext("2d");
- ctx.scale(dpr, dpr);
- return { canvas, ctx, dpr };
- }
- function iconFromCanvas(canvas, dpr, anchor) {
- return new Icon({
- src: canvas.toDataURL(),
- scale: 1 / dpr,
- anchor,
- anchorXUnits: "fraction",
- anchorYUnits: "fraction",
- });
- }
- function createZoneLabelStyle(name) {
- const text = String(name || "");
- const font = "13px sans-serif";
- const padX = 10;
- const padY = 4;
- const measure = document.createElement("canvas").getContext("2d");
- measure.font = font;
- const textWidth = Math.ceil(measure.measureText(text).width);
- const boxW = textWidth + padX * 2;
- const boxH = 21;
- const { canvas, ctx, dpr } = createHiDpiCanvas(boxW + 8, boxH + 8);
- const x = 4;
- const y = 4;
- ctx.shadowColor = "rgba(0, 0, 0, 0.12)";
- ctx.shadowBlur = 4;
- ctx.shadowOffsetY = 1;
- roundRectPath(ctx, x, y, boxW, boxH, 4);
- ctx.fillStyle = "#ffffff";
- ctx.fill();
- ctx.shadowColor = "transparent";
- ctx.fillStyle = "#1F1F1F";
- ctx.font = font;
- ctx.textAlign = "center";
- ctx.textBaseline = "middle";
- ctx.fillText(text, x + boxW / 2, y + boxH / 2);
- return new Style({
- image: iconFromCanvas(canvas, dpr, [0.5, 0.5]),
- });
- }
- function drawCoverImage(ctx, img, x, y, size) {
- const scale = Math.max(size / img.width, size / img.height);
- const dw = img.width * scale;
- const dh = img.height * scale;
- ctx.drawImage(img, x + (size - dw) / 2, y + (size - dh) / 2, dw, dh);
- }
- function createPhotoMarkerStyle(img, count) {
- const thumb = 42;
- const arrowH = 6;
- const arrowW = 10;
- const badgeH = 16;
- const countText = String(count ?? "");
- const badgeW = Math.max(16, 8 + countText.length * 7);
- const cssW = thumb + 12;
- const cssH = 6 + thumb + arrowH;
- const { canvas, ctx, dpr } = createHiDpiCanvas(cssW, cssH);
- const thumbX = (cssW - thumb) / 2;
- const thumbY = 6;
- ctx.save();
- roundRectPath(ctx, thumbX, thumbY, thumb, thumb, 6);
- ctx.clip();
- if (img) {
- drawCoverImage(ctx, img, thumbX, thumbY, thumb);
- } else {
- ctx.fillStyle = "#d9d9d9";
- ctx.fillRect(thumbX, thumbY, thumb, thumb);
- }
- ctx.restore();
- roundRectPath(ctx, thumbX, thumbY, thumb, thumb, 6);
- ctx.strokeStyle = "#ffffff";
- ctx.lineWidth = 2;
- ctx.stroke();
- const ax = thumbX + thumb / 2;
- const ay = thumbY + thumb - 1;
- ctx.beginPath();
- ctx.moveTo(ax - arrowW / 2, ay);
- ctx.lineTo(ax + arrowW / 2, ay);
- ctx.lineTo(ax, ay + arrowH);
- ctx.closePath();
- ctx.fillStyle = "#ffffff";
- ctx.fill();
- const bx = thumbX + thumb - badgeW + 4;
- const by = thumbY - 6;
- roundRectPath(ctx, bx, by, badgeW, badgeH, 8);
- ctx.fillStyle = "#ffffff";
- ctx.fill();
- ctx.fillStyle = "#1F1F1F";
- ctx.font = "10px sans-serif";
- ctx.textAlign = "center";
- ctx.textBaseline = "middle";
- ctx.fillText(countText, bx + badgeW / 2, by + badgeH / 2 + 0.5);
- return new Style({
- image: iconFromCanvas(canvas, dpr, [0.5, 1]),
- });
- }
- function loadImage(src) {
- return new Promise((resolve) => {
- if (!src) {
- resolve(null);
- return;
- }
- const img = new Image();
- if (/^https?:\/\//i.test(String(src))) {
- img.crossOrigin = "anonymous";
- }
- img.onload = () => resolve(img);
- img.onerror = () => resolve(null);
- img.src = src;
- });
- }
- function createPointGeometry(lng, lat, projection) {
- const geometry = new Point([lng, lat]);
- if (projection && projection.getCode() !== "EPSG:4326") {
- geometry.transform("EPSG:4326", projection);
- }
- return geometry;
- }
- function getItemPolygon(item) {
- const wkt = item?.polygon ?? item?.geom ?? item?.geomWkt;
- return typeof wkt === "string" ? wkt.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]);
- }
- 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;
- }
- }
- class FileMap {
- constructor() {
- this._pending = null;
- this._pendingMarkers = null;
- this._fitTimer = null;
- this._renderToken = 0;
- this.baseImageLayers = [];
- this.baseImageVisible = false;
- this.currentFarmId = null;
- const vectorStyle = new KMap.VectorStyle();
- this.recordPolygonLayer = new KMap.VectorLayer("fileRecordPolygonLayer", 1000, {
- minZoom: 8,
- maxZoom: 22,
- source: new VectorSource({}),
- style: (f) => {
- const colors = RECORD_TYPE_STYLE[f.get("styleType")] || RECORD_TYPE_STYLE.zone;
- const polygonStyle = vectorStyle.getPolygonStyle(colors.fill, colors.stroke, 2);
- const label = f.get("zone_name");
- if (!label || f.get("styleType") === "album") 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 }),
- }),
- }),
- ];
- },
- });
- this.albumMarkerLayer = new KMap.VectorLayer("fileAlbumMarkerLayer", 1001, {
- minZoom: 8,
- maxZoom: 22,
- source: new VectorSource({}),
- });
- }
- initBaseImageLayers() {
- FARM_BASE_IMAGE_LAYERS.forEach((item) => {
- const imgLayer = new StaticImgLayer(
- item.url,
- {
- extent: item.extent,
- minZoom: 8,
- maxZoom: 22,
- opacity: 0.4,
- },
- 4,
- this.kmap,
- );
- this.baseImageLayers.push({ farmId: item.farmId, layer: imgLayer });
- });
- this.applyBaseImageVisibility();
- }
- applyBaseImageVisibility() {
- const canShow = this.baseImageVisible && hasFarmBaseImage(this.currentFarmId);
- this.baseImageLayers.forEach(({ farmId, layer }) => {
- const shouldShow = canShow && farmId === this.currentFarmId;
- if (shouldShow) {
- layer.show();
- } else {
- layer.hide();
- }
- });
- }
- showBaseImageByFarmId(farmId) {
- this.currentFarmId = farmId != null && farmId !== "" ? Number(farmId) : null;
- if (!hasFarmBaseImage(this.currentFarmId)) {
- this.baseImageVisible = false;
- }
- this.applyBaseImageVisibility();
- }
- toggleBaseImageLayers(visible = !this.baseImageVisible) {
- this.baseImageVisible = visible && hasFarmBaseImage(this.currentFarmId);
- this.applyBaseImageVisibility();
- }
- 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();
- this.flushPending();
- 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.initBaseImageLayers();
- this.kmap.addLayer(this.recordPolygonLayer.layer);
- this.kmap.addLayer(this.albumMarkerLayer.layer);
- this.flushPending();
- }
- flushPending() {
- if (this._pending) {
- const pending = this._pending;
- this._pending = null;
- this.setRecordPolygons(pending.records, pending.tabKey);
- }
- if (this._pendingMarkers) {
- const pending = this._pendingMarkers;
- this._pendingMarkers = null;
- this.setAlbumMarkers(pending);
- }
- }
- setRecordPolygons(records, tabKey = "phenology") {
- if (!this.kmap) {
- this._pending = { records, tabKey };
- return;
- }
- if (this._fitTimer) {
- clearTimeout(this._fitTimer);
- this._fitTimer = null;
- }
- const renderToken = ++this._renderToken;
- 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();
- list.forEach((item) => {
- const wkt = getItemPolygon(item);
- if (!wkt || seenWkt.has(wkt)) return;
- seenWkt.add(wkt);
- try {
- source.addFeature(createRecordFeature(wkt, item, projection, tabKey));
- } catch (e) {
- console.warn("[FileMap] polygon parse failed", e);
- }
- });
- this.recordPolygonLayer.layer.changed();
- source.changed();
- this.fitView(renderToken);
- }
- setAlbumMarkers({ labels = [], photos = [] } = {}) {
- if (!this.kmap) {
- this._pendingMarkers = { labels, photos };
- return;
- }
- const source = this.albumMarkerLayer.source;
- source.clear(true);
- const projection = this.kmap.map.getView().getProjection();
- labels.forEach((item) => {
- if (!item?.name || item.longitude == null || item.latitude == null) return;
- const feature = new Feature({
- geometry: createPointGeometry(item.longitude, item.latitude, projection),
- });
- feature.setStyle(createZoneLabelStyle(item.name));
- source.addFeature(feature);
- });
- photos.forEach((item) => {
- if (item.longitude == null || item.latitude == null) return;
- const feature = new Feature({
- geometry: createPointGeometry(item.longitude, item.latitude, projection),
- });
- feature.setStyle(createPhotoMarkerStyle(null, item.count));
- source.addFeature(feature);
- loadImage(item.cover).then((img) => {
- feature.setStyle(createPhotoMarkerStyle(img, item.count));
- });
- });
- this.albumMarkerLayer.layer.changed();
- source.changed();
- }
- 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;
- }
- 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);
- }
- }
- }
- export default FileMap;
|