|
|
@@ -1,6 +1,6 @@
|
|
|
<template>
|
|
|
<div class="map-container">
|
|
|
- <div id="map" class="map"></div>
|
|
|
+ <div ref="mapRoot" class="map"></div>
|
|
|
<!-- 以下为悬浮信息等 -->
|
|
|
<div
|
|
|
v-if="hoverInfo"
|
|
|
@@ -10,13 +10,18 @@
|
|
|
@mouseleave="onHoverLeave"
|
|
|
>
|
|
|
<h3>{{ hoverInfo.fieldName }}</h3>
|
|
|
- <div>
|
|
|
+ <div v-if="hoverInfo.isCluster">
|
|
|
<p>
|
|
|
+ <span>提示</span><span>点击聚合点放大查看</span>
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ <div v-else>
|
|
|
+ <!-- <p>
|
|
|
<span>状态:</span
|
|
|
><span>{{
|
|
|
hoverInfo.analysisState === 30 ? "已完成分析" : "未完成分析"
|
|
|
}}</span>
|
|
|
- </p>
|
|
|
+ </p> -->
|
|
|
<p>
|
|
|
<span>风场编号</span><span>{{ hoverInfo.codeNumber }}</span>
|
|
|
</p>
|
|
|
@@ -28,7 +33,8 @@
|
|
|
><span
|
|
|
>{{
|
|
|
newWind?.ratedCapacityNumber ? newWind.ratedCapacityNumber : 0
|
|
|
- }}/KW</span
|
|
|
+ }}
|
|
|
+ MW</span
|
|
|
>
|
|
|
</p>
|
|
|
<p>
|
|
|
@@ -37,7 +43,7 @@
|
|
|
<p>
|
|
|
<span>纬度</span><span>{{ hoverInfo.latitude }}°</span>
|
|
|
</p>
|
|
|
- <p>
|
|
|
+ <!-- <p>
|
|
|
<span>分析模型数</span
|
|
|
><span
|
|
|
>{{
|
|
|
@@ -76,7 +82,7 @@
|
|
|
? $formatDateTWO(newWind.dataEndTime)
|
|
|
: "暂无数据"
|
|
|
}}</span>
|
|
|
- </p>
|
|
|
+ </p> -->
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
@@ -97,7 +103,7 @@
|
|
|
><span
|
|
|
>{{
|
|
|
hoverfengji?.ratedCapacity ? hoverfengji?.ratedCapacity : 0
|
|
|
- }}/KW</span
|
|
|
+ }}kW</span
|
|
|
>
|
|
|
</p>
|
|
|
<p>
|
|
|
@@ -172,26 +178,39 @@ import "ol/ol.css";
|
|
|
import { Map, View, Feature } from "ol";
|
|
|
import TileLayer from "ol/layer/Tile.js";
|
|
|
import { XYZ } from "ol/source";
|
|
|
-import { fromLonLat } from "ol/proj";
|
|
|
+import { fromLonLat, transformExtent } from "ol/proj";
|
|
|
import { Vector } from "ol/source";
|
|
|
+import Cluster from "ol/source/Cluster";
|
|
|
import { Vector as VectorLayer } from "ol/layer";
|
|
|
import { Point } from "ol/geom";
|
|
|
-import { Icon, Style, Stroke, Fill, Text } from "ol/style";
|
|
|
+import { Icon, Style, Stroke, Fill, Circle as CircleStyle, Text } from "ol/style";
|
|
|
+import { boundingExtent } from "ol/extent";
|
|
|
import ZoomSlider from "ol/control/ZoomSlider.js";
|
|
|
import { defaults as defaultControls } from "ol/control.js";
|
|
|
import GeoJSON from "ol/format/GeoJSON";
|
|
|
-import { ChinaData, ProvinceData } from "china-map-geojson";
|
|
|
|
|
|
// 假设你已经有正确的山西省边界数据文件(GeoJSON 格式)
|
|
|
// import shanxiBoundary from "./shanxi.json";
|
|
|
import shanxiBoundary from "./jilin.json";
|
|
|
-import icon01 from "../../assets/img/iconFC.png";
|
|
|
-import icon02 from "../../assets/img/iconFC.png";
|
|
|
-import icon03 from "../../assets/img/iconFC.png";
|
|
|
-import icon04 from "../../assets/img/iconFJ.png";
|
|
|
-import icon05 from "../../assets/img/icon05.png";
|
|
|
-import icon06 from "../../assets/img/iconFJ.png";
|
|
|
-import defaultIcon from "../../assets/img/iconFJ.png";
|
|
|
+// import icon01 from "../../assets/img/iconFC.png";
|
|
|
+// import icon02 from "../../assets/img/iconFC.png";
|
|
|
+// import icon03 from "../../assets/img/iconFC.png";
|
|
|
+// 地图点图标:按 addMarker 的 val / 风场 analysisState 选用
|
|
|
+import icon01 from "../../assets/img/iconFJ.png"; // 风场:未完成分析 / 排队中(val: -1、10)
|
|
|
+import icon02 from "../../assets/img/iconFJ.png"; // 风场:正在分析中(val: 0、20)
|
|
|
+import icon03 from "../../assets/img/iconFJ.png"; // 风场:已完成分析(val: 1、30)
|
|
|
+import icon04 from "../../assets/img/iconFJ.png"; // 风机点(val: "4",scale 0.5)
|
|
|
+import icon05 from "../../assets/img/icon05.png"; // 测风塔(val: "5")
|
|
|
+import icon06 from "../../assets/img/iconFJ.png"; // 异常风机(val: "6",点击弹出异常描述)
|
|
|
+import defaultIcon from "../../assets/img/iconFJ.png"; // 未匹配到 val 时的兜底图标
|
|
|
+
|
|
|
+// import icon01 from "../../assets/img/icon01.png";
|
|
|
+// import icon02 from "../../assets/img/icon02.png";
|
|
|
+// import icon03 from "../../assets/img/icon03.png";
|
|
|
+// import icon04 from "../../assets/img/icon04.png";
|
|
|
+// import icon05 from "../../assets/img/icon05.png";
|
|
|
+// import icon06 from "../../assets/img/iconFJ.png";
|
|
|
+// import defaultIcon from "../../assets/img/iconFJ.png";
|
|
|
|
|
|
import { queryErrDescByEngine, queryFloatingWindowInfo } from "@/api/ledger.js";
|
|
|
export default {
|
|
|
@@ -204,6 +223,7 @@ export default {
|
|
|
name: "T-map",
|
|
|
data() {
|
|
|
return {
|
|
|
+ map: null,
|
|
|
dialogVisible: false,
|
|
|
tableData: [],
|
|
|
hoverInfo: null,
|
|
|
@@ -232,153 +252,140 @@ export default {
|
|
|
fengjiTimer: null,
|
|
|
taTimer: null,
|
|
|
newWind: {},
|
|
|
- provinceLabelLayer: null,
|
|
|
- cityLabelLayer: null,
|
|
|
- currentZoom: 3,
|
|
|
};
|
|
|
},
|
|
|
mounted() {
|
|
|
+ // 与 map/tiles 一致:Google XYZ(EPSG:3857),路径 {z}/{x}/{y},Y 轴北向为 0
|
|
|
this.map = new Map({
|
|
|
- target: "map",
|
|
|
+ target: this.$refs.mapRoot,
|
|
|
view: new View({
|
|
|
- projection: "EPSG:4326",
|
|
|
+ projection: "EPSG:3857",
|
|
|
center: fromLonLat([116.389, 39.903]),
|
|
|
zoom: 3,
|
|
|
minZoom: 3,
|
|
|
- maxZoom: 15,
|
|
|
- extent: [69, 18, 136, 54],
|
|
|
+ maxZoom: 13,
|
|
|
+ extent: transformExtent([69, 18, 136, 54], "EPSG:4326", "EPSG:3857"),
|
|
|
}),
|
|
|
layers: [
|
|
|
new TileLayer({
|
|
|
source: new XYZ({
|
|
|
url: process.env.VUE_APP_MAPVIEW,
|
|
|
+ projection: "EPSG:3857",
|
|
|
+ tileSize: 256,
|
|
|
+ maxZoom: 13,
|
|
|
}),
|
|
|
}),
|
|
|
+ ...(process.env.VUE_APP_MAPVIEW_ADMIN
|
|
|
+ ? [
|
|
|
+ new TileLayer({
|
|
|
+ source: new XYZ({
|
|
|
+ url: process.env.VUE_APP_MAPVIEW_ADMIN,
|
|
|
+ projection: "EPSG:3857",
|
|
|
+ tileSize: 256,
|
|
|
+ // admin 只切到 12 级;13 级放大 12 级图,避免请求空瓦片导致区县名消失
|
|
|
+ maxZoom: 13,
|
|
|
+ }),
|
|
|
+ }),
|
|
|
+ ]
|
|
|
+ : []),
|
|
|
+ ...(process.env.VUE_APP_MAPVIEW_PLACES
|
|
|
+ ? [
|
|
|
+ new TileLayer({
|
|
|
+ // OpenLayers 的 minZoom 是 exclusive:11 表示从 12 级起显示
|
|
|
+ minZoom: 11,
|
|
|
+ source: new XYZ({
|
|
|
+ url: process.env.VUE_APP_MAPVIEW_PLACES,
|
|
|
+ projection: "EPSG:3857",
|
|
|
+ tileSize: 256,
|
|
|
+ minZoom: 12,
|
|
|
+ maxZoom: 13,
|
|
|
+ }),
|
|
|
+ }),
|
|
|
+ ]
|
|
|
+ : []),
|
|
|
+ new VectorLayer({
|
|
|
+ id: "farm-marker",
|
|
|
+ source: this.createFarmClusterSource(),
|
|
|
+ zIndex: 8,
|
|
|
+ style: (feature) => this.styleFarmCluster(feature),
|
|
|
+ }),
|
|
|
new VectorLayer({
|
|
|
id: "marker",
|
|
|
source: new Vector(),
|
|
|
+ zIndex: 9,
|
|
|
}),
|
|
|
],
|
|
|
controls: defaultControls().extend([new ZoomSlider()]),
|
|
|
});
|
|
|
|
|
|
- this.initAdminNameLayers();
|
|
|
- this.map.getView().on("change:resolution", () => {
|
|
|
- this.currentZoom = this.map.getView().getZoom() || 3;
|
|
|
- if (this.provinceLabelLayer) this.provinceLabelLayer.changed();
|
|
|
- if (this.cityLabelLayer) this.cityLabelLayer.changed();
|
|
|
- });
|
|
|
- this.initEvent();
|
|
|
- },
|
|
|
- methods: {
|
|
|
- initAdminNameLayers() {
|
|
|
- const format = new GeoJSON();
|
|
|
- const provinceSource = new Vector();
|
|
|
- const citySource = new Vector();
|
|
|
-
|
|
|
- // 省级标注
|
|
|
- format
|
|
|
- .readFeatures(ChinaData, {
|
|
|
- dataProjection: "EPSG:4326",
|
|
|
- featureProjection: "EPSG:4326",
|
|
|
- })
|
|
|
- .forEach((feature) => {
|
|
|
- const props = feature.getProperties() || {};
|
|
|
- let coord = null;
|
|
|
- if (Array.isArray(props.cp) && props.cp.length >= 2) {
|
|
|
- coord = props.cp;
|
|
|
- } else if (props.longitude != null && props.latitude != null) {
|
|
|
- coord = [Number(props.longitude), Number(props.latitude)];
|
|
|
- }
|
|
|
- if (!coord || !props.name) return;
|
|
|
- const pointFeature = new Feature({
|
|
|
- geometry: new Point(coord),
|
|
|
- name: props.name,
|
|
|
- level: "province",
|
|
|
- });
|
|
|
- provinceSource.addFeature(pointFeature);
|
|
|
- });
|
|
|
+ // -------------【添加山西省真实边界线】------------- 定位全国的时候注释这个代码
|
|
|
+ // if (this.$route.path === "/home/cockpitManage") {
|
|
|
+ // // 通过导入的 JSON 文件加载边界数据(GeoJSON 格式),并解析成矢量要素
|
|
|
+ // const shanxiSource = new Vector({
|
|
|
+ // features: new GeoJSON().readFeatures(shanxiBoundary, {
|
|
|
+ // featureProjection: "EPSG:4326",
|
|
|
+ // }),
|
|
|
+ // });
|
|
|
+ // // 创建矢量图层,仅显示边界线(填充颜色设为透明)
|
|
|
+ // const shanxiLayer = new VectorLayer({
|
|
|
+ // source: shanxiSource,
|
|
|
+ // style: new Style({
|
|
|
+ // stroke: new Stroke({
|
|
|
+ // color: "rgba(59, 130, 246, 0.5)",
|
|
|
+ // width: 3,
|
|
|
+ // }),
|
|
|
+ // fill: new Fill({
|
|
|
+ // color: "rgba(59, 130, 246, 0.05)", // 透明填充
|
|
|
+ // }),
|
|
|
+ // }),
|
|
|
+ // });
|
|
|
+ // this.map.addLayer(shanxiLayer);
|
|
|
|
|
|
- // 地市/州标注(china-map-geojson 最细到地市级;不含乡镇)
|
|
|
- Object.values(ProvinceData || {}).forEach((collection) => {
|
|
|
- if (!collection || !collection.features) return;
|
|
|
- format
|
|
|
- .readFeatures(collection, {
|
|
|
- dataProjection: "EPSG:4326",
|
|
|
- featureProjection: "EPSG:4326",
|
|
|
- })
|
|
|
- .forEach((feature) => {
|
|
|
- const props = feature.getProperties() || {};
|
|
|
- const cp = props.cp;
|
|
|
- if (!Array.isArray(cp) || cp.length < 2 || !props.name) return;
|
|
|
- const pointFeature = new Feature({
|
|
|
- geometry: new Point(cp),
|
|
|
- name: props.name,
|
|
|
- level: "city",
|
|
|
- });
|
|
|
- citySource.addFeature(pointFeature);
|
|
|
- });
|
|
|
- });
|
|
|
+ // const markerLayer = this.map
|
|
|
+ // .getLayers()
|
|
|
+ // .getArray()
|
|
|
+ // .find((layer) => {
|
|
|
+ // return layer.get("id") === "marker";
|
|
|
+ // });
|
|
|
+ // if (markerLayer) {
|
|
|
+ // markerLayer.setZIndex(10);
|
|
|
+ // }
|
|
|
|
|
|
- this.provinceLabelLayer = new VectorLayer({
|
|
|
- source: provinceSource,
|
|
|
- zIndex: 20,
|
|
|
- declutter: true,
|
|
|
- style: (feature) => this.createAdminLabelStyle(feature, "province"),
|
|
|
- });
|
|
|
- this.cityLabelLayer = new VectorLayer({
|
|
|
- source: citySource,
|
|
|
- zIndex: 21,
|
|
|
- declutter: true,
|
|
|
- style: (feature) => this.createAdminLabelStyle(feature, "city"),
|
|
|
- });
|
|
|
- this.map.addLayer(this.provinceLabelLayer);
|
|
|
- this.map.addLayer(this.cityLabelLayer);
|
|
|
- },
|
|
|
- createAdminLabelStyle(feature, level) {
|
|
|
- const zoom = this.currentZoom;
|
|
|
- const name = feature.get("name") || "";
|
|
|
- if (level === "province") {
|
|
|
- // 放大到地市级后隐藏省名,减少重叠
|
|
|
- if (zoom >= 6.2) return null;
|
|
|
- return new Style({
|
|
|
- text: new Text({
|
|
|
- text: name,
|
|
|
- font: zoom >= 4.8 ? "bold 13px Microsoft YaHei" : "12px Microsoft YaHei",
|
|
|
- fill: new Fill({ color: "rgba(20, 40, 70, 0.92)" }),
|
|
|
- stroke: new Stroke({
|
|
|
- color: "rgba(255, 255, 255, 0.92)",
|
|
|
- width: 3,
|
|
|
- }),
|
|
|
- overflow: true,
|
|
|
- }),
|
|
|
- });
|
|
|
- }
|
|
|
- // 地市名称:中高缩放显示
|
|
|
- if (zoom < 5.2) return null;
|
|
|
- const isCountyLike = /[县市区旗]$/.test(name) || name.includes("自治");
|
|
|
- // 更高缩放才尽量展示偏县级命名,减轻全国视图拥挤
|
|
|
- if (zoom < 6.5 && isCountyLike && name.length > 4) {
|
|
|
- // 仍显示,但字号略小
|
|
|
+ // // 同时设置高亮图层 zIndex 较低
|
|
|
+ // shanxiLayer.setZIndex(1);
|
|
|
+ // // -------------【结束】-------------
|
|
|
+ // // const targetExtent = [106.8, 34.3, 118.6, 41.2];//山西
|
|
|
+ // const targetExtent = [119.65, 40.87, 131.31, 46.30];//吉林
|
|
|
+ // this.map.getView().fit(targetExtent, { duration: 2000 });
|
|
|
+ // }
|
|
|
+ // 这个放在外面
|
|
|
+ this.initEvent();
|
|
|
+ this.bindClusterZoom();
|
|
|
+ this.$nextTick(() => {
|
|
|
+ if (this.map) {
|
|
|
+ this.map.updateSize();
|
|
|
}
|
|
|
- return new Style({
|
|
|
- text: new Text({
|
|
|
- text: name,
|
|
|
- font:
|
|
|
- zoom >= 7
|
|
|
- ? "12px Microsoft YaHei"
|
|
|
- : "11px Microsoft YaHei",
|
|
|
- fill: new Fill({
|
|
|
- color: "rgba(30, 55, 90, 0.9)",
|
|
|
- }),
|
|
|
- stroke: new Stroke({
|
|
|
- color: "rgba(255, 255, 255, 0.9)",
|
|
|
- width: 2.5,
|
|
|
- }),
|
|
|
- overflow: true,
|
|
|
- }),
|
|
|
- });
|
|
|
- },
|
|
|
+ });
|
|
|
+ },
|
|
|
+ activated() {
|
|
|
+ if (!this.map) return;
|
|
|
+ this.map.setTarget(this.$refs.mapRoot || null);
|
|
|
+ this.$nextTick(() => {
|
|
|
+ this.map && this.map.updateSize();
|
|
|
+ });
|
|
|
+ },
|
|
|
+ deactivated() {
|
|
|
+ if (this.map) {
|
|
|
+ this.map.setTarget(null);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ this.unbindClusterZoom();
|
|
|
+ if (this.map) {
|
|
|
+ this.map.setTarget(null);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
onHoverLeave() {
|
|
|
this.hovering = false;
|
|
|
clearTimeout(this.hideTimer);
|
|
|
@@ -408,75 +415,312 @@ export default {
|
|
|
}, 100);
|
|
|
},
|
|
|
|
|
|
- addMarker(data = { point: [120.2, 30.35], val: "1" }) {
|
|
|
- const layer = this.map
|
|
|
+ createFarmClusterSource() {
|
|
|
+ this.farmSource = new Vector();
|
|
|
+ this.clusterSource = new Cluster({
|
|
|
+ distance: 80,
|
|
|
+ minDistance: 36,
|
|
|
+ source: this.farmSource,
|
|
|
+ wrapX: false,
|
|
|
+ });
|
|
|
+ return this.clusterSource;
|
|
|
+ },
|
|
|
+ bindClusterZoom() {
|
|
|
+ if (!this.map) return;
|
|
|
+ this._onClusterResolutionChange = () => {
|
|
|
+ this.updateClusterDistance();
|
|
|
+ const farmLayer = this.getLayerById("farm-marker");
|
|
|
+ if (farmLayer) farmLayer.changed();
|
|
|
+ };
|
|
|
+ this.map
|
|
|
+ .getView()
|
|
|
+ .on("change:resolution", this._onClusterResolutionChange);
|
|
|
+ this.updateClusterDistance();
|
|
|
+ },
|
|
|
+ unbindClusterZoom() {
|
|
|
+ if (!this.map || !this._onClusterResolutionChange) return;
|
|
|
+ this.map
|
|
|
+ .getView()
|
|
|
+ .un("change:resolution", this._onClusterResolutionChange);
|
|
|
+ this._onClusterResolutionChange = null;
|
|
|
+ },
|
|
|
+ updateClusterDistance() {
|
|
|
+ if (!this.clusterSource || !this.map) return;
|
|
|
+ const zoom = this.map.getView().getZoom() || 4;
|
|
|
+ if (zoom >= 10) {
|
|
|
+ this.clusterSource.setDistance(0);
|
|
|
+ } else if (zoom >= 8) {
|
|
|
+ this.clusterSource.setDistance(40);
|
|
|
+ } else if (zoom >= 6) {
|
|
|
+ this.clusterSource.setDistance(56);
|
|
|
+ } else {
|
|
|
+ this.clusterSource.setDistance(72);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ getLayerById(id) {
|
|
|
+ if (!this.map) return null;
|
|
|
+ return this.map
|
|
|
.getLayers()
|
|
|
.getArray()
|
|
|
- .find((element) => {
|
|
|
- return element.get("id") === "marker";
|
|
|
- });
|
|
|
- const source = layer.getSource();
|
|
|
-
|
|
|
- const iconSrc = this.getIconForValue(data.val);
|
|
|
- const scale = data.val === "4" ? [0.5, 0.5] : [0.3, 0.3];
|
|
|
+ .find((element) => element.get("id") === id);
|
|
|
+ },
|
|
|
+ isDetailMarker(val) {
|
|
|
+ return val === "4" || val === 4 || val === "5" || val === 5 || val === "6" || val === 6;
|
|
|
+ },
|
|
|
+ getClusterMembers(feature) {
|
|
|
+ const members = feature && feature.get("features");
|
|
|
+ if (Array.isArray(members) && members.length) {
|
|
|
+ return members;
|
|
|
+ }
|
|
|
+ return feature ? [feature] : [];
|
|
|
+ },
|
|
|
+ readThemeColor(name, fallback) {
|
|
|
+ if (typeof window === "undefined") return fallback;
|
|
|
+ const value = getComputedStyle(document.documentElement)
|
|
|
+ .getPropertyValue(name)
|
|
|
+ .trim();
|
|
|
+ return value || fallback;
|
|
|
+ },
|
|
|
+ parseRgb(color) {
|
|
|
+ const value = String(color || "").trim();
|
|
|
+ if (value.startsWith("rgb")) {
|
|
|
+ const parts = value.match(/[\d.]+/g) || [];
|
|
|
+ return {
|
|
|
+ r: Number(parts[0]) || 0,
|
|
|
+ g: Number(parts[1]) || 0,
|
|
|
+ b: Number(parts[2]) || 0,
|
|
|
+ };
|
|
|
+ }
|
|
|
+ const raw = value.replace("#", "");
|
|
|
+ if (raw.length !== 3 && raw.length !== 6) {
|
|
|
+ return { r: 61, g: 135, b: 157 };
|
|
|
+ }
|
|
|
+ const full =
|
|
|
+ raw.length === 3
|
|
|
+ ? raw
|
|
|
+ .split("")
|
|
|
+ .map((item) => item + item)
|
|
|
+ .join("")
|
|
|
+ : raw;
|
|
|
+ const n = parseInt(full, 16);
|
|
|
+ return {
|
|
|
+ r: (n >> 16) & 255,
|
|
|
+ g: (n >> 8) & 255,
|
|
|
+ b: n & 255,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ mixRgb(rgb, target, amount) {
|
|
|
+ const t = Math.min(1, Math.max(0, amount));
|
|
|
+ return {
|
|
|
+ r: Math.round(rgb.r + (target - rgb.r) * t),
|
|
|
+ g: Math.round(rgb.g + (target - rgb.g) * t),
|
|
|
+ b: Math.round(rgb.b + (target - rgb.b) * t),
|
|
|
+ };
|
|
|
+ },
|
|
|
+ rgba(rgb, alpha) {
|
|
|
+ return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${alpha})`;
|
|
|
+ },
|
|
|
+ colorWithAlpha(color, alpha) {
|
|
|
+ return this.rgba(this.parseRgb(color), alpha);
|
|
|
+ },
|
|
|
+ getClusterRadius(count) {
|
|
|
+ return Math.round(
|
|
|
+ Math.max(16, Math.min(38, 12 + Math.sqrt(count) * 3.6)),
|
|
|
+ );
|
|
|
+ },
|
|
|
+ buildClusterStyles(count) {
|
|
|
+ const radius = this.getClusterRadius(count);
|
|
|
+ const primary = this.parseRgb(
|
|
|
+ this.readThemeColor("--primary-color", "#3D879D"),
|
|
|
+ );
|
|
|
+ const core = this.mixRgb(primary, 0, 0.28);
|
|
|
+ const glow = this.mixRgb(primary, 255, 0.45);
|
|
|
+ const ring = this.mixRgb(primary, 255, 0.62);
|
|
|
+ const label = count > 99 ? "99+" : String(count);
|
|
|
+ const fontSize = radius >= 32 ? 16 : radius >= 24 ? 13 : 11;
|
|
|
|
|
|
+ return [
|
|
|
+ new Style({
|
|
|
+ image: new CircleStyle({
|
|
|
+ radius: radius + 8,
|
|
|
+ fill: new Fill({ color: this.rgba(glow, 0.18) }),
|
|
|
+ }),
|
|
|
+ }),
|
|
|
+ new Style({
|
|
|
+ image: new CircleStyle({
|
|
|
+ radius: radius + 3,
|
|
|
+ stroke: new Stroke({
|
|
|
+ color: this.rgba(ring, 0.85),
|
|
|
+ width: 2.5,
|
|
|
+ }),
|
|
|
+ }),
|
|
|
+ }),
|
|
|
+ new Style({
|
|
|
+ image: new CircleStyle({
|
|
|
+ radius,
|
|
|
+ fill: new Fill({ color: this.rgba(core, 0.82) }),
|
|
|
+ stroke: new Stroke({
|
|
|
+ color: "rgba(255, 255, 255, 0.9)",
|
|
|
+ width: 1.5,
|
|
|
+ }),
|
|
|
+ }),
|
|
|
+ }),
|
|
|
+ new Style({
|
|
|
+ image: new CircleStyle({
|
|
|
+ radius: Math.max(3, radius * 0.22),
|
|
|
+ fill: new Fill({ color: "rgba(255, 255, 255, 0.28)" }),
|
|
|
+ displacement: [0, -radius * 0.36],
|
|
|
+ }),
|
|
|
+ }),
|
|
|
+ new Style({
|
|
|
+ text: new Text({
|
|
|
+ text: label,
|
|
|
+ font: `600 ${fontSize}px "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif`,
|
|
|
+ fill: new Fill({ color: "#ffffff" }),
|
|
|
+ stroke: new Stroke({
|
|
|
+ color: this.rgba(core, 0.9),
|
|
|
+ width: 3.5,
|
|
|
+ }),
|
|
|
+ offsetY: 0.5,
|
|
|
+ }),
|
|
|
+ }),
|
|
|
+ ];
|
|
|
+ },
|
|
|
+ getFarmIconSizeByZoom() {
|
|
|
+ const zoom = (this.map && this.map.getView().getZoom()) || 4;
|
|
|
+ if (zoom < 5) return { width: 16, height: 20 };
|
|
|
+ if (zoom < 7) return { width: 22, height: 28 };
|
|
|
+ if (zoom < 9) return { width: 28, height: 35 };
|
|
|
+ return { width: 36, height: 45 };
|
|
|
+ },
|
|
|
+ styleFarmCluster(feature) {
|
|
|
+ const members = this.getClusterMembers(feature);
|
|
|
+ if (members.length > 1) {
|
|
|
+ return this.buildClusterStyles(members.length);
|
|
|
+ }
|
|
|
+ const data = members[0] && members[0].get("data");
|
|
|
+ const iconSize = this.getFarmIconSizeByZoom();
|
|
|
+ return new Style({
|
|
|
+ image: new Icon({
|
|
|
+ src: this.getIconForValue(data && data.val),
|
|
|
+ width: iconSize.width,
|
|
|
+ height: iconSize.height,
|
|
|
+ anchor: [0.5, 1],
|
|
|
+ opacity: 1,
|
|
|
+ }),
|
|
|
+ });
|
|
|
+ },
|
|
|
+ zoomToCluster(members) {
|
|
|
+ if (!this.map || !members.length) return;
|
|
|
+ const coordinates = members.map((item) =>
|
|
|
+ item.getGeometry().getCoordinates(),
|
|
|
+ );
|
|
|
+ this.map.getView().fit(boundingExtent(coordinates), {
|
|
|
+ duration: 450,
|
|
|
+ padding: [72, 72, 72, 72],
|
|
|
+ maxZoom: 9,
|
|
|
+ });
|
|
|
+ },
|
|
|
+ fitMarkers(animate = true) {
|
|
|
+ if (!this.map || !this.farmSource) return;
|
|
|
+ const features = this.farmSource.getFeatures();
|
|
|
+ if (!features.length) return;
|
|
|
+ if (features.length === 1) {
|
|
|
+ this.map.getView().animate({
|
|
|
+ center: features[0].getGeometry().getCoordinates(),
|
|
|
+ zoom: 8,
|
|
|
+ duration: animate ? 600 : 0,
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const extent = boundingExtent(
|
|
|
+ features.map((item) => item.getGeometry().getCoordinates()),
|
|
|
+ );
|
|
|
+ this.map.getView().fit(extent, {
|
|
|
+ duration: animate ? 600 : 0,
|
|
|
+ padding: [56, 56, 56, 56],
|
|
|
+ maxZoom: 6,
|
|
|
+ });
|
|
|
+ },
|
|
|
+ addMarker(data = { point: [120.2, 30.35], val: "1" }) {
|
|
|
const feature = new Feature({
|
|
|
- geometry: new Point(fromLonLat(data.point, "EPSG:4326")),
|
|
|
+ geometry: new Point(fromLonLat(data.point)),
|
|
|
name: "marker",
|
|
|
data,
|
|
|
});
|
|
|
|
|
|
- feature.setStyle(
|
|
|
- new Style({
|
|
|
- image: new Icon({
|
|
|
- src: iconSrc,
|
|
|
- scale: scale,
|
|
|
- anchor: [0.5, 1],
|
|
|
- opacity: 1,
|
|
|
+ if (this.isDetailMarker(data.val)) {
|
|
|
+ const layer = this.getLayerById("marker");
|
|
|
+ if (!layer) return;
|
|
|
+ const iconSrc = this.getIconForValue(data.val);
|
|
|
+ const iconSize = this.getMarkerIconSize(data.val);
|
|
|
+ feature.setStyle(
|
|
|
+ new Style({
|
|
|
+ image: new Icon({
|
|
|
+ src: iconSrc,
|
|
|
+ width: iconSize.width,
|
|
|
+ height: iconSize.height,
|
|
|
+ anchor: [0.5, 1],
|
|
|
+ opacity: 1,
|
|
|
+ }),
|
|
|
}),
|
|
|
- })
|
|
|
- );
|
|
|
+ );
|
|
|
+ layer.getSource().addFeature(feature);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- source.addFeature(feature);
|
|
|
+ if (this.farmSource) {
|
|
|
+ this.farmSource.addFeature(feature);
|
|
|
+ }
|
|
|
},
|
|
|
clearMarkers() {
|
|
|
- const layer = this.map
|
|
|
- .getLayers()
|
|
|
- .getArray()
|
|
|
- .find((element) => {
|
|
|
- return element.get("id") === "marker";
|
|
|
- });
|
|
|
+ if (this.farmSource) {
|
|
|
+ this.farmSource.clear();
|
|
|
+ }
|
|
|
+ const layer = this.getLayerById("marker");
|
|
|
if (layer) {
|
|
|
- const source = layer.getSource();
|
|
|
- source.clear();
|
|
|
+ layer.getSource().clear();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /**
|
|
|
+ * 按屏幕像素定图标大小。iconFJ2 为 1024px,不能再用 scale(0.2 仍约 200px)。
|
|
|
+ */
|
|
|
+ getMarkerIconSize(val) {
|
|
|
+ if (val === "4" || val === 4 || val === "6" || val === 6) {
|
|
|
+ return { width: 40, height: 50 };
|
|
|
+ }
|
|
|
+ if (val === "5" || val === 5) {
|
|
|
+ return { width: 30, height: 40 };
|
|
|
}
|
|
|
+ return { width: 40, height: 50 };
|
|
|
},
|
|
|
+ // val:驾驶舱风场用 analysisState;风场详情页 4=风机 5=测风塔 6=异常风机
|
|
|
getIconForValue(val) {
|
|
|
switch (val) {
|
|
|
case "-1":
|
|
|
case -1:
|
|
|
- return icon01;
|
|
|
+ return icon01; // 未完成分析
|
|
|
case "10":
|
|
|
case 10:
|
|
|
- return icon01;
|
|
|
+ return icon01; // 排队中
|
|
|
case 0:
|
|
|
case "0":
|
|
|
- return icon02;
|
|
|
+ return icon02; // 正在分析
|
|
|
case 20:
|
|
|
case "20":
|
|
|
- return icon02;
|
|
|
+ return icon02; // 正在分析
|
|
|
case 1:
|
|
|
case "1":
|
|
|
- return icon03;
|
|
|
+ return icon03; // 已完成分析
|
|
|
case 30:
|
|
|
case "30":
|
|
|
- return icon03;
|
|
|
+ return icon03; // 已完成分析
|
|
|
case "4":
|
|
|
- return icon04;
|
|
|
+ return icon04; // 风机
|
|
|
case "5":
|
|
|
- return icon05;
|
|
|
+ return icon05; // 测风塔
|
|
|
case "6":
|
|
|
- return icon06;
|
|
|
+ return icon06; // 异常风机
|
|
|
default:
|
|
|
return defaultIcon;
|
|
|
}
|
|
|
@@ -505,11 +749,37 @@ export default {
|
|
|
};
|
|
|
this.map.on("pointermove", (evt) => {
|
|
|
const features = this.map.getFeaturesAtPixel(evt.pixel, {
|
|
|
- hitTolerance: 1,
|
|
|
+ hitTolerance: 8,
|
|
|
});
|
|
|
+ const mapEl = this.map.getTargetElement();
|
|
|
if (features && features.length > 0) {
|
|
|
+ if (mapEl) mapEl.style.cursor = "pointer";
|
|
|
const feature = features.at(0);
|
|
|
- const data = feature.get("data");
|
|
|
+ const members = this.getClusterMembers(feature);
|
|
|
+ if (members.length > 1) {
|
|
|
+ if (lastHoveredFeature !== feature) {
|
|
|
+ const hoverHeight = 90;
|
|
|
+ const hoverWidth = 200;
|
|
|
+ this.hoverInfo = {
|
|
|
+ isCluster: true,
|
|
|
+ fieldName: `${members.length} 座风场`,
|
|
|
+ };
|
|
|
+ this.newWind = {};
|
|
|
+ this.hoverStyle.left = `${calculateHoverLeft(
|
|
|
+ evt.pixel[0],
|
|
|
+ hoverWidth,
|
|
|
+ )}px`;
|
|
|
+ this.hoverStyle.top = `${calculateHoverTop(
|
|
|
+ evt.pixel[1],
|
|
|
+ hoverHeight,
|
|
|
+ )}px`;
|
|
|
+ this.hoverfengji = null;
|
|
|
+ this.hoverta = null;
|
|
|
+ lastHoveredFeature = feature;
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const data = members[0] && members[0].get("data");
|
|
|
// 如果没有 data 属性,则不处理,但允许事件冒泡
|
|
|
if (!data) {
|
|
|
return;
|
|
|
@@ -548,6 +818,7 @@ export default {
|
|
|
lastHoveredFeature = feature;
|
|
|
}
|
|
|
} else {
|
|
|
+ if (mapEl) mapEl.style.cursor = "";
|
|
|
// 没有特征时,清空所有悬浮信息
|
|
|
if (!this.hoveringInfo) this.hoverInfo = null;
|
|
|
if (!this.hoveringFengji) this.hoverfengji = null;
|
|
|
@@ -561,11 +832,16 @@ export default {
|
|
|
// click 事件处理
|
|
|
this.map.on("click", (evt) => {
|
|
|
const features = this.map.getFeaturesAtPixel(evt.pixel, {
|
|
|
- hitTolerance: 1,
|
|
|
+ hitTolerance: 8,
|
|
|
});
|
|
|
if (features && features.length > 0) {
|
|
|
const feature = features.at(0);
|
|
|
- const data = feature.get("data");
|
|
|
+ const members = this.getClusterMembers(feature);
|
|
|
+ if (members.length > 1) {
|
|
|
+ this.zoomToCluster(members);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const data = members[0] && members[0].get("data");
|
|
|
// 如果没有 data,直接返回,让事件继续冒泡
|
|
|
if (!data) {
|
|
|
return;
|
|
|
@@ -595,7 +871,7 @@ export default {
|
|
|
});
|
|
|
},
|
|
|
getwind() {
|
|
|
- // if (this.hoverInfo && this.hoverInfo.batchCode) {
|
|
|
+ if (!this.hoverInfo || this.hoverInfo.isCluster) return;
|
|
|
const param = {
|
|
|
batchcode: this.hoverInfo.batchCode,
|
|
|
fieldCode: this.hoverInfo.codeNumber,
|
|
|
@@ -611,10 +887,10 @@ export default {
|
|
|
// console.warn("hoverInfo 或 batchCode 未定义");
|
|
|
// }
|
|
|
},
|
|
|
- moveAndZoom(data = { point: [120.2, 30.35], zoom: 15 }) {
|
|
|
+ moveAndZoom(data = { point: [120.2, 30.35], zoom: 12 }) {
|
|
|
this.map.getView().animate({
|
|
|
- center: fromLonLat(data.point, "EPSG:4326"),
|
|
|
- zoom: data.zoom,
|
|
|
+ center: fromLonLat(data.point),
|
|
|
+ zoom: Math.min(Number(data.zoom) || 13, 13),
|
|
|
duration: 2000,
|
|
|
});
|
|
|
},
|
|
|
@@ -637,46 +913,74 @@ export default {
|
|
|
|
|
|
.hover-info {
|
|
|
position: absolute;
|
|
|
- color: white;
|
|
|
+ color: var(--map-hover-text, #303133);
|
|
|
padding: 0;
|
|
|
z-index: 100;
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
h3 {
|
|
|
- background-color: var(--header-bg);
|
|
|
+ background-color: var(--map-hover-header-bg, var(--primary-color, #008080));
|
|
|
+ color: var(--map-hover-header-text, #ffffff);
|
|
|
width: 240px;
|
|
|
padding: 5px 10px;
|
|
|
font-size: 16px;
|
|
|
+ margin: 0;
|
|
|
}
|
|
|
+
|
|
|
div {
|
|
|
- background-color: var(--content-bg);
|
|
|
+ background-color: var(--map-hover-body-bg, rgba(255, 255, 255, 0.96));
|
|
|
+ color: var(--map-hover-text, #303133);
|
|
|
width: 240px;
|
|
|
font-size: 12px;
|
|
|
+
|
|
|
p {
|
|
|
padding: 5px 10px;
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
+ margin: 0;
|
|
|
+
|
|
|
+ span:first-child {
|
|
|
+ color: var(--map-hover-label-text, #606266);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
.hover-fengji {
|
|
|
position: absolute;
|
|
|
- color: white;
|
|
|
+ color: var(--map-hover-text, #303133);
|
|
|
padding: 0;
|
|
|
z-index: 100;
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+
|
|
|
h3 {
|
|
|
- background-color: var(--header-bg);
|
|
|
+ background-color: var(--map-hover-header-bg, var(--primary-color, #008080));
|
|
|
+ color: var(--map-hover-header-text, #ffffff);
|
|
|
width: 240px;
|
|
|
padding: 5px 10px;
|
|
|
font-size: 16px;
|
|
|
+ margin: 0;
|
|
|
}
|
|
|
+
|
|
|
div {
|
|
|
- background-color: var(--content-bg);
|
|
|
+ background-color: var(--map-hover-body-bg, rgba(255, 255, 255, 0.96));
|
|
|
+ color: var(--map-hover-text, #303133);
|
|
|
width: 240px;
|
|
|
font-size: 12px;
|
|
|
+
|
|
|
p {
|
|
|
padding: 5px 10px;
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
+ margin: 0;
|
|
|
+
|
|
|
+ span:first-child {
|
|
|
+ color: var(--map-hover-label-text, #606266);
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|