|
@@ -5,9 +5,11 @@ import StaticImgLayer from "@/utils/ol-map/StaticImgLayer";
|
|
|
import { Vector as VectorSource } from "ol/source.js";
|
|
import { Vector as VectorSource } from "ol/source.js";
|
|
|
import Style from "ol/style/Style";
|
|
import Style from "ol/style/Style";
|
|
|
import Text from "ol/style/Text";
|
|
import Text from "ol/style/Text";
|
|
|
|
|
+import Icon from "ol/style/Icon";
|
|
|
import { Fill, Stroke } from "ol/style";
|
|
import { Fill, Stroke } from "ol/style";
|
|
|
import { WKT } from "ol/format";
|
|
import { WKT } from "ol/format";
|
|
|
import Feature from "ol/Feature";
|
|
import Feature from "ol/Feature";
|
|
|
|
|
+import Point from "ol/geom/Point";
|
|
|
import {
|
|
import {
|
|
|
createEmpty,
|
|
createEmpty,
|
|
|
extend as extendExtent,
|
|
extend as extendExtent,
|
|
@@ -46,19 +48,168 @@ const RECORD_TYPE_STYLE = {
|
|
|
zone: { fill: "rgba(28, 158, 128, 0.45)", stroke: "#1c9e80" },
|
|
zone: { fill: "rgba(28, 158, 128, 0.45)", stroke: "#1c9e80" },
|
|
|
growth: { fill: "rgba(255, 120, 0, 0.5)", stroke: "#cc5500" },
|
|
growth: { fill: "rgba(255, 120, 0, 0.5)", stroke: "#cc5500" },
|
|
|
pest: { fill: "rgba(224, 49, 49, 0.45)", stroke: "#e03131" },
|
|
pest: { fill: "rgba(224, 49, 49, 0.45)", stroke: "#e03131" },
|
|
|
|
|
+ album: { fill: "rgba(255, 255, 255, 0.28)", stroke: "#ffffff" },
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-/** 物候→管理分区,农事→管理分区,异常→病虫害异常 */
|
|
|
|
|
|
|
+/** 物候→管理分区,农事→管理分区,异常→病虫害异常,相册→白色分区 */
|
|
|
const TAB_STYLE_TYPE = {
|
|
const TAB_STYLE_TYPE = {
|
|
|
phenology: "zone",
|
|
phenology: "zone",
|
|
|
farming: "zone",
|
|
farming: "zone",
|
|
|
abnormal: "pest",
|
|
abnormal: "pest",
|
|
|
|
|
+ album: "album",
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
export const RECORD_TAB_KEYS = ["phenology", "farming", "abnormal"];
|
|
export const RECORD_TAB_KEYS = ["phenology", "farming", "abnormal"];
|
|
|
|
|
|
|
|
const DEFAULT_CENTER = "POINT(113.6142086995688 23.585836479509055)";
|
|
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) {
|
|
function getItemPolygon(item) {
|
|
|
const wkt = item?.polygon ?? item?.geom ?? item?.geomWkt;
|
|
const wkt = item?.polygon ?? item?.geom ?? item?.geomWkt;
|
|
|
return typeof wkt === "string" ? wkt.trim() : "";
|
|
return typeof wkt === "string" ? wkt.trim() : "";
|
|
@@ -127,6 +278,7 @@ export function recordsToCenterPoint(records) {
|
|
|
class FileMap {
|
|
class FileMap {
|
|
|
constructor() {
|
|
constructor() {
|
|
|
this._pending = null;
|
|
this._pending = null;
|
|
|
|
|
+ this._pendingMarkers = null;
|
|
|
this._fitTimer = null;
|
|
this._fitTimer = null;
|
|
|
this._renderToken = 0;
|
|
this._renderToken = 0;
|
|
|
this.baseImageLayers = [];
|
|
this.baseImageLayers = [];
|
|
@@ -140,9 +292,9 @@ class FileMap {
|
|
|
source: new VectorSource({}),
|
|
source: new VectorSource({}),
|
|
|
style: (f) => {
|
|
style: (f) => {
|
|
|
const colors = RECORD_TYPE_STYLE[f.get("styleType")] || RECORD_TYPE_STYLE.zone;
|
|
const colors = RECORD_TYPE_STYLE[f.get("styleType")] || RECORD_TYPE_STYLE.zone;
|
|
|
- const polygonStyle = vectorStyle.getPolygonStyle(colors.fill, colors.stroke, 3);
|
|
|
|
|
|
|
+ const polygonStyle = vectorStyle.getPolygonStyle(colors.fill, colors.stroke, 2);
|
|
|
const label = f.get("zone_name");
|
|
const label = f.get("zone_name");
|
|
|
- if (!label) return [polygonStyle];
|
|
|
|
|
|
|
+ if (!label || f.get("styleType") === "album") return [polygonStyle];
|
|
|
return [
|
|
return [
|
|
|
polygonStyle,
|
|
polygonStyle,
|
|
|
new Style({
|
|
new Style({
|
|
@@ -156,6 +308,11 @@ class FileMap {
|
|
|
];
|
|
];
|
|
|
},
|
|
},
|
|
|
});
|
|
});
|
|
|
|
|
+ this.albumMarkerLayer = new KMap.VectorLayer("fileAlbumMarkerLayer", 1001, {
|
|
|
|
|
+ minZoom: 8,
|
|
|
|
|
+ maxZoom: 22,
|
|
|
|
|
+ source: new VectorSource({}),
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
initBaseImageLayers() {
|
|
initBaseImageLayers() {
|
|
@@ -209,11 +366,7 @@ class FileMap {
|
|
|
if (this.kmap?.map) {
|
|
if (this.kmap?.map) {
|
|
|
this.kmap.map.setTarget(target);
|
|
this.kmap.map.setTarget(target);
|
|
|
this.kmap.map.updateSize();
|
|
this.kmap.map.updateSize();
|
|
|
- if (this._pending) {
|
|
|
|
|
- const pending = this._pending;
|
|
|
|
|
- this._pending = null;
|
|
|
|
|
- this.setRecordPolygons(pending.records, pending.tabKey);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ this.flushPending();
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -221,12 +374,21 @@ class FileMap {
|
|
|
this.kmap.addXYZLayer(config.base_img_url3 + "map/lby/{z}/{x}/{y}.png", { minZoom: 8, maxZoom: 22 }, 2);
|
|
this.kmap.addXYZLayer(config.base_img_url3 + "map/lby/{z}/{x}/{y}.png", { minZoom: 8, maxZoom: 22 }, 2);
|
|
|
this.initBaseImageLayers();
|
|
this.initBaseImageLayers();
|
|
|
this.kmap.addLayer(this.recordPolygonLayer.layer);
|
|
this.kmap.addLayer(this.recordPolygonLayer.layer);
|
|
|
|
|
+ this.kmap.addLayer(this.albumMarkerLayer.layer);
|
|
|
|
|
+ this.flushPending();
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
|
|
+ flushPending() {
|
|
|
if (this._pending) {
|
|
if (this._pending) {
|
|
|
const pending = this._pending;
|
|
const pending = this._pending;
|
|
|
this._pending = null;
|
|
this._pending = null;
|
|
|
this.setRecordPolygons(pending.records, pending.tabKey);
|
|
this.setRecordPolygons(pending.records, pending.tabKey);
|
|
|
}
|
|
}
|
|
|
|
|
+ if (this._pendingMarkers) {
|
|
|
|
|
+ const pending = this._pendingMarkers;
|
|
|
|
|
+ this._pendingMarkers = null;
|
|
|
|
|
+ this.setAlbumMarkers(pending);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
setRecordPolygons(records, tabKey = "phenology") {
|
|
setRecordPolygons(records, tabKey = "phenology") {
|
|
@@ -264,6 +426,41 @@ class FileMap {
|
|
|
this.fitView(renderToken);
|
|
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) {
|
|
fitView(renderToken) {
|
|
|
if (!this.kmap?.map) return;
|
|
if (!this.kmap?.map) return;
|
|
|
if (renderToken != null && renderToken !== this._renderToken) return;
|
|
if (renderToken != null && renderToken !== this._renderToken) return;
|