|
@@ -0,0 +1,1284 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="tech-blue-province-map">
|
|
|
|
|
+ <div class="tech-blue-province-map__bg" aria-hidden="true"></div>
|
|
|
|
|
+ <div class="tech-blue-province-map__grid" aria-hidden="true"></div>
|
|
|
|
|
+ <div class="tech-blue-province-map__stage">
|
|
|
|
|
+ <div class="tech-blue-province-map__shadow" aria-hidden="true"></div>
|
|
|
|
|
+ <div ref="mapRoot" class="tech-blue-province-map__canvas"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="tech-blue-province-map__south-sea" aria-hidden="true">
|
|
|
|
|
+ <span class="tech-blue-province-map__south-sea-title">南海诸岛</span>
|
|
|
|
|
+ <i
|
|
|
|
|
+ v-for="n in 7"
|
|
|
|
|
+ :key="'dot-' + n"
|
|
|
|
|
+ class="tech-blue-province-map__south-sea-dot"
|
|
|
|
|
+ :style="southSeaDotStyle(n)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-if="hoverInfo"
|
|
|
|
|
+ class="tech-blue-province-map__hover"
|
|
|
|
|
+ :style="hoverStyle"
|
|
|
|
|
+ @mouseenter="hovering = true"
|
|
|
|
|
+ @mouseleave="onHoverLeave"
|
|
|
|
|
+ >
|
|
|
|
|
+ <h3>{{ hoverInfo.fieldName || hoverInfo.companyName || "风场信息" }}</h3>
|
|
|
|
|
+ <div class="tech-blue-province-map__hover-body">
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <span>状态</span>
|
|
|
|
|
+ <span>{{
|
|
|
|
|
+ hoverInfo.analysisState === 30 || hoverInfo.val === 30 || hoverInfo.val === "30"
|
|
|
|
|
+ ? "已完成分析"
|
|
|
|
|
+ : hoverInfo.analysisState === 0 || hoverInfo.val === 0 || hoverInfo.val === "0"
|
|
|
|
|
+ ? "分析中"
|
|
|
|
|
+ : "未完成分析"
|
|
|
|
|
+ }}</span>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <span>风场编号</span>
|
|
|
|
|
+ <span>{{ hoverInfo.codeNumber || "-" }}</span>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <span>风机数量</span>
|
|
|
|
|
+ <span>{{ newWind.engineTotalCount || 0 }} 台</span>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <span>总容量</span>
|
|
|
|
|
+ <span>{{ newWind.ratedCapacityNumber || 0 }}/KW</span>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <span>经度</span>
|
|
|
|
|
+ <span>{{ hoverInfo.longitude || hoverInfo.point?.[0] || "-" }}°</span>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <span>纬度</span>
|
|
|
|
|
+ <span>{{ hoverInfo.latitude || hoverInfo.point?.[1] || "-" }}°</span>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <span>分析模型数</span>
|
|
|
|
|
+ <span>{{ hoverInfo.analysisTypeCount || 0 }} 个</span>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <span>分析开始时间</span>
|
|
|
|
|
+ <span>{{
|
|
|
|
|
+ newWind.analysisStartTime
|
|
|
|
|
+ ? $formatDateTWO(newWind.analysisStartTime)
|
|
|
|
|
+ : "暂无数据"
|
|
|
|
|
+ }}</span>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <span>分析结束时间</span>
|
|
|
|
|
+ <span>{{
|
|
|
|
|
+ newWind.analysisEndTime
|
|
|
|
|
+ ? $formatDateTWO(newWind.analysisEndTime)
|
|
|
|
|
+ : "暂无数据"
|
|
|
|
|
+ }}</span>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <span>数据开始时间</span>
|
|
|
|
|
+ <span>{{
|
|
|
|
|
+ newWind.dataStartTime
|
|
|
|
|
+ ? $formatDateTWO(newWind.dataStartTime)
|
|
|
|
|
+ : "暂无数据"
|
|
|
|
|
+ }}</span>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ <span>数据结束时间</span>
|
|
|
|
|
+ <span>{{
|
|
|
|
|
+ newWind.dataEndTime
|
|
|
|
|
+ ? $formatDateTWO(newWind.dataEndTime)
|
|
|
|
|
+ : "暂无数据"
|
|
|
|
|
+ }}</span>
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import { Map, View, Feature } from "ol";
|
|
|
|
|
+import GeoJSON from "ol/format/GeoJSON";
|
|
|
|
|
+import { defaults as defaultControls } from "ol/control";
|
|
|
|
|
+import { LineString, Point } from "ol/geom";
|
|
|
|
|
+import { Vector as VectorLayer } from "ol/layer";
|
|
|
|
|
+import { Vector as VectorSource } from "ol/source";
|
|
|
|
|
+import {
|
|
|
|
|
+ Fill,
|
|
|
|
|
+ Icon,
|
|
|
|
|
+ Stroke,
|
|
|
|
|
+ Style,
|
|
|
|
|
+ Text,
|
|
|
|
|
+} from "ol/style";
|
|
|
|
|
+import { ChinaData, ProvinceData } from "china-map-geojson";
|
|
|
|
|
+import { queryFloatingWindowInfo } from "@/api/ledger.js";
|
|
|
|
|
+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";
|
|
|
|
|
+
|
|
|
|
|
+const DEFAULT_CENTER = [104.5, 35.5];
|
|
|
|
|
+const DEFAULT_ZOOM = 5.15;
|
|
|
|
|
+
|
|
|
|
|
+/** 分层挤出:仅向下偏移,避免横向错位看起来发斜 */
|
|
|
|
|
+const EXTRUSION_STEPS = [
|
|
|
|
|
+ {
|
|
|
|
|
+ dx: 0,
|
|
|
|
|
+ dy: -0.55,
|
|
|
|
|
+ fill: "rgba(1, 8, 26, 0.99)",
|
|
|
|
|
+ stroke: "rgba(8, 55, 130, 0.42)",
|
|
|
|
|
+ width: 1.35,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ dx: 0,
|
|
|
|
|
+ dy: -0.42,
|
|
|
|
|
+ fill: "rgba(3, 14, 40, 0.98)",
|
|
|
|
|
+ stroke: "rgba(14, 80, 160, 0.48)",
|
|
|
|
|
+ width: 1.25,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ dx: 0,
|
|
|
|
|
+ dy: -0.3,
|
|
|
|
|
+ fill: "rgba(6, 22, 55, 0.96)",
|
|
|
|
|
+ stroke: "rgba(22, 115, 195, 0.55)",
|
|
|
|
|
+ width: 1.15,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ dx: 0,
|
|
|
|
|
+ dy: -0.18,
|
|
|
|
|
+ fill: "rgba(10, 34, 82, 0.94)",
|
|
|
|
|
+ stroke: "rgba(40, 150, 230, 0.62)",
|
|
|
|
|
+ width: 1.05,
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ dx: 0,
|
|
|
|
|
+ dy: -0.08,
|
|
|
|
|
+ fill: "rgba(26, 72, 155, 0.86)",
|
|
|
|
|
+ stroke: "rgba(110, 220, 255, 0.8)",
|
|
|
|
|
+ width: 0.85,
|
|
|
|
|
+ },
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const CN_TO_PROVINCE_KEY = {
|
|
|
|
|
+ 安徽: "Anhui",
|
|
|
|
|
+ 澳门: "Aomen",
|
|
|
|
|
+ 北京: "Beijing",
|
|
|
|
|
+ 重庆: "Chongqing",
|
|
|
|
|
+ 福建: "Fujian",
|
|
|
|
|
+ 贵州: "Guizhou",
|
|
|
|
|
+ 广西: "Guangxi",
|
|
|
|
|
+ 广东: "Guangdong",
|
|
|
|
|
+ 甘肃: "Gansu",
|
|
|
|
|
+ 湖南: "Hunan",
|
|
|
|
|
+ 湖北: "Hubei",
|
|
|
|
|
+ 黑龙江: "Heilongjiang",
|
|
|
|
|
+ 河北: "Hebei",
|
|
|
|
|
+ 河南: "Henan",
|
|
|
|
|
+ 海南: "Hainan",
|
|
|
|
|
+ 江西: "Jiangxi",
|
|
|
|
|
+ 江苏: "Jiangsu",
|
|
|
|
|
+ 吉林: "Jilin",
|
|
|
|
|
+ 辽宁: "Liaoning",
|
|
|
|
|
+ 宁夏: "Ningxia",
|
|
|
|
|
+ 内蒙古: "Neimenggu",
|
|
|
|
|
+ 青海: "Qinghai",
|
|
|
|
|
+ 四川: "Sichuan",
|
|
|
|
|
+ 上海: "Shanghai",
|
|
|
|
|
+ 山东: "Shandong",
|
|
|
|
|
+ 山西: "Shanxi_1",
|
|
|
|
|
+ 陕西: "Shanxi_3",
|
|
|
|
|
+ 天津: "Tianjin",
|
|
|
|
|
+ 台湾: "Taiwan",
|
|
|
|
|
+ 新疆: "Xinjiang",
|
|
|
|
|
+ 香港: "Xianggang",
|
|
|
|
|
+ 西藏: "Xizang",
|
|
|
|
|
+ 云南: "Yunnan",
|
|
|
|
|
+ 浙江: "Zhejiang",
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const RIVER_LINES = [
|
|
|
|
|
+ [
|
|
|
|
|
+ [96.5, 35.2],
|
|
|
|
|
+ [101.8, 34.6],
|
|
|
|
|
+ [106.4, 33.9],
|
|
|
|
|
+ [110.8, 31.6],
|
|
|
|
|
+ [114.9, 30.6],
|
|
|
|
|
+ [118.5, 31.0],
|
|
|
|
|
+ [121.7, 31.2],
|
|
|
|
|
+ ],
|
|
|
|
|
+ [
|
|
|
|
|
+ [103.6, 36.2],
|
|
|
|
|
+ [108.1, 38.2],
|
|
|
|
|
+ [111.5, 38.7],
|
|
|
|
|
+ [115.0, 37.9],
|
|
|
|
|
+ [118.4, 37.6],
|
|
|
|
|
+ ],
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+const RAIL_LINES = [
|
|
|
|
|
+ [
|
|
|
|
|
+ [87.6, 43.8],
|
|
|
|
|
+ [96.8, 40.2],
|
|
|
|
|
+ [103.8, 36.2],
|
|
|
|
|
+ [108.9, 34.3],
|
|
|
|
|
+ [113.6, 34.7],
|
|
|
|
|
+ [116.4, 39.9],
|
|
|
|
|
+ [121.5, 31.2],
|
|
|
|
|
+ ],
|
|
|
|
|
+ [
|
|
|
|
|
+ [111.7, 40.8],
|
|
|
|
|
+ [112.5, 37.9],
|
|
|
|
|
+ [113.3, 34.8],
|
|
|
|
|
+ [114.3, 30.6],
|
|
|
|
|
+ [113.3, 23.1],
|
|
|
|
|
+ ],
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: "TechBlueOfflineProvinceGeoMap",
|
|
|
|
|
+ props: {
|
|
|
|
|
+ markers: {
|
|
|
|
|
+ type: Array,
|
|
|
|
|
+ default: () => [],
|
|
|
|
|
+ },
|
|
|
|
|
+ maplist: {
|
|
|
|
|
+ type: [Array, Object],
|
|
|
|
|
+ default: () => [],
|
|
|
|
|
+ },
|
|
|
|
|
+ activeProvinceNames: {
|
|
|
|
|
+ type: Array,
|
|
|
|
|
+ default: () => [],
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ map: null,
|
|
|
|
|
+ markerSource: null,
|
|
|
|
|
+ markerLayer: null,
|
|
|
|
|
+ provinceLayer: null,
|
|
|
|
|
+ provinceGlowLayer: null,
|
|
|
|
|
+ countyLayer: null,
|
|
|
|
|
+ cityLabelLayer: null,
|
|
|
|
|
+ cityLabelSource: null,
|
|
|
|
|
+ provinceFeatures: [],
|
|
|
|
|
+ markerFeatures: [],
|
|
|
|
|
+ focusedProvinceName: "",
|
|
|
|
|
+ focusedCityName: "",
|
|
|
|
|
+ currentZoom: DEFAULT_ZOOM,
|
|
|
|
|
+ hoverInfo: null,
|
|
|
|
|
+ hovering: false,
|
|
|
|
|
+ hideTimer: null,
|
|
|
|
|
+ hoverStyle: {
|
|
|
|
|
+ position: "absolute",
|
|
|
|
|
+ left: "0px",
|
|
|
|
|
+ top: "0px",
|
|
|
|
|
+ },
|
|
|
|
|
+ newWind: {},
|
|
|
|
|
+ lastHoveredFeature: null,
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+ mounted() {
|
|
|
|
|
+ this.initMap();
|
|
|
|
|
+ this.setMarkers(this.markers);
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ if (this.map) {
|
|
|
|
|
+ this.map.updateSize();
|
|
|
|
|
+ this.fitChina(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ window.addEventListener("resize", this.handleResize);
|
|
|
|
|
+ },
|
|
|
|
|
+ beforeDestroy() {
|
|
|
|
|
+ window.removeEventListener("resize", this.handleResize);
|
|
|
|
|
+ if (this.map) {
|
|
|
|
|
+ this.map.setTarget(null);
|
|
|
|
|
+ this.map = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ watch: {
|
|
|
|
|
+ markers: {
|
|
|
|
|
+ deep: true,
|
|
|
|
|
+ handler(val) {
|
|
|
|
|
+ this.setMarkers(val || []);
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ activeProvinceNames: {
|
|
|
|
|
+ deep: true,
|
|
|
|
|
+ handler() {
|
|
|
|
|
+ this.refreshProvinceStyle();
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ handleResize() {
|
|
|
|
|
+ if (this.map) this.map.updateSize();
|
|
|
|
|
+ },
|
|
|
|
|
+ southSeaDotStyle(n) {
|
|
|
|
|
+ const positions = [
|
|
|
|
|
+ [22, 38],
|
|
|
|
|
+ [38, 52],
|
|
|
|
|
+ [52, 34],
|
|
|
|
|
+ [64, 58],
|
|
|
|
|
+ [48, 72],
|
|
|
|
|
+ [72, 46],
|
|
|
|
|
+ [34, 68],
|
|
|
|
|
+ ];
|
|
|
|
|
+ const [left, top] = positions[n - 1] || [40, 40];
|
|
|
|
|
+ return {
|
|
|
|
|
+ left: `${left}%`,
|
|
|
|
|
+ top: `${top}%`,
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+ cloneTranslatedFeatures(features, dx, dy) {
|
|
|
|
|
+ return features.map((feature) => {
|
|
|
|
|
+ const cloned = feature.clone();
|
|
|
|
|
+ const geometry = cloned.getGeometry();
|
|
|
|
|
+ if (geometry) {
|
|
|
|
|
+ geometry.translate(dx, dy);
|
|
|
|
|
+ }
|
|
|
|
|
+ return cloned;
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ createExtrusionLayers(baseFeatures) {
|
|
|
|
|
+ return EXTRUSION_STEPS.map((step, index) => {
|
|
|
|
|
+ const source = new VectorSource({
|
|
|
|
|
+ features: this.cloneTranslatedFeatures(
|
|
|
|
|
+ baseFeatures,
|
|
|
|
|
+ step.dx,
|
|
|
|
|
+ step.dy,
|
|
|
|
|
+ ),
|
|
|
|
|
+ });
|
|
|
|
|
+ return new VectorLayer({
|
|
|
|
|
+ source,
|
|
|
|
|
+ zIndex: index + 1,
|
|
|
|
|
+ style: new Style({
|
|
|
|
|
+ fill: new Fill({ color: step.fill }),
|
|
|
|
|
+ stroke: new Stroke({
|
|
|
|
|
+ color: step.stroke,
|
|
|
|
|
+ width: step.width,
|
|
|
|
|
+ }),
|
|
|
|
|
+ }),
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ initMap() {
|
|
|
|
|
+ const format = new GeoJSON();
|
|
|
|
|
+ const provinceSource = new VectorSource({
|
|
|
|
|
+ features: format.readFeatures(ChinaData, {
|
|
|
|
|
+ dataProjection: "EPSG:4326",
|
|
|
|
|
+ featureProjection: "EPSG:4326",
|
|
|
|
|
+ }),
|
|
|
|
|
+ });
|
|
|
|
|
+ this.provinceFeatures = provinceSource.getFeatures();
|
|
|
|
|
+
|
|
|
|
|
+ const glowSource = new VectorSource({
|
|
|
|
|
+ features: this.provinceFeatures.map((feature) => feature.clone()),
|
|
|
|
|
+ });
|
|
|
|
|
+ const countySource = this.createCountySource(format);
|
|
|
|
|
+ const decorSource = this.createDecorSource();
|
|
|
|
|
+ const extrusionLayers = this.createExtrusionLayers(this.provinceFeatures);
|
|
|
|
|
+
|
|
|
|
|
+ const markerSource = new VectorSource();
|
|
|
|
|
+ this.markerSource = markerSource;
|
|
|
|
|
+
|
|
|
|
|
+ this.cityLabelSource = new VectorSource();
|
|
|
|
|
+ this.cityLabelLayer = new VectorLayer({
|
|
|
|
|
+ source: this.cityLabelSource,
|
|
|
|
|
+ zIndex: 45,
|
|
|
|
|
+ style: (feature) => this.createCityLabelStyle(feature),
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const seaLayer = new VectorLayer({
|
|
|
|
|
+ source: decorSource,
|
|
|
|
|
+ zIndex: 0,
|
|
|
|
|
+ style: (feature) => this.createDecorStyle(feature),
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.provinceGlowLayer = new VectorLayer({
|
|
|
|
|
+ source: glowSource,
|
|
|
|
|
+ zIndex: 20,
|
|
|
|
|
+ style: (feature) => this.createProvinceGlowStyle(feature),
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.provinceLayer = new VectorLayer({
|
|
|
|
|
+ source: provinceSource,
|
|
|
|
|
+ zIndex: 30,
|
|
|
|
|
+ style: (feature) => this.createProvinceStyle(feature),
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.countyLayer = new VectorLayer({
|
|
|
|
|
+ source: countySource,
|
|
|
|
|
+ zIndex: 35,
|
|
|
|
|
+ style: (feature) => this.createCountyBoundaryStyle(feature),
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const markerLayer = new VectorLayer({
|
|
|
|
|
+ source: markerSource,
|
|
|
|
|
+ zIndex: 50,
|
|
|
|
|
+ style: (feature) => this.createMarkerStyle(feature),
|
|
|
|
|
+ });
|
|
|
|
|
+ this.markerLayer = markerLayer;
|
|
|
|
|
+
|
|
|
|
|
+ this.map = new Map({
|
|
|
|
|
+ target: this.$refs.mapRoot,
|
|
|
|
|
+ layers: [
|
|
|
|
|
+ seaLayer,
|
|
|
|
|
+ ...extrusionLayers,
|
|
|
|
|
+ this.provinceGlowLayer,
|
|
|
|
|
+ this.provinceLayer,
|
|
|
|
|
+ this.countyLayer,
|
|
|
|
|
+ this.cityLabelLayer,
|
|
|
|
|
+ markerLayer,
|
|
|
|
|
+ ],
|
|
|
|
|
+ view: new View({
|
|
|
|
|
+ projection: "EPSG:4326",
|
|
|
|
|
+ center: DEFAULT_CENTER,
|
|
|
|
|
+ zoom: DEFAULT_ZOOM,
|
|
|
|
|
+ minZoom: 3.2,
|
|
|
|
|
+ maxZoom: 10,
|
|
|
|
|
+ constrainResolution: false,
|
|
|
|
|
+ }),
|
|
|
|
|
+ controls: defaultControls({
|
|
|
|
|
+ zoom: true,
|
|
|
|
|
+ attribution: false,
|
|
|
|
|
+ rotate: false,
|
|
|
|
|
+ }),
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.map.getView().on("change:resolution", () => {
|
|
|
|
|
+ this.currentZoom = this.map.getView().getZoom() || DEFAULT_ZOOM;
|
|
|
|
|
+ this.refreshProvinceStyle();
|
|
|
|
|
+ if (this.cityLabelLayer) this.cityLabelLayer.changed();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.initMapClick();
|
|
|
|
|
+ this.initPointerMove();
|
|
|
|
|
+ this.fitChina(false);
|
|
|
|
|
+ },
|
|
|
|
|
+ createDecorSource() {
|
|
|
|
|
+ const source = new VectorSource();
|
|
|
|
|
+ RIVER_LINES.forEach((coords, index) => {
|
|
|
|
|
+ source.addFeature(
|
|
|
|
|
+ new Feature({
|
|
|
|
|
+ geometry: new LineString(coords),
|
|
|
|
|
+ decorType: "river",
|
|
|
|
|
+ decorIndex: index,
|
|
|
|
|
+ }),
|
|
|
|
|
+ );
|
|
|
|
|
+ });
|
|
|
|
|
+ RAIL_LINES.forEach((coords, index) => {
|
|
|
|
|
+ source.addFeature(
|
|
|
|
|
+ new Feature({
|
|
|
|
|
+ geometry: new LineString(coords),
|
|
|
|
|
+ decorType: "rail",
|
|
|
|
|
+ decorIndex: index,
|
|
|
|
|
+ }),
|
|
|
|
|
+ );
|
|
|
|
|
+ });
|
|
|
|
|
+ return source;
|
|
|
|
|
+ },
|
|
|
|
|
+ createCountySource(format) {
|
|
|
|
|
+ const featureCollections = Object.values(ProvinceData || {});
|
|
|
|
|
+ const featureList = [];
|
|
|
|
|
+ featureCollections.forEach((collection) => {
|
|
|
|
|
+ if (!collection) return;
|
|
|
|
|
+ const features = format.readFeatures(collection, {
|
|
|
|
|
+ dataProjection: "EPSG:4326",
|
|
|
|
|
+ featureProjection: "EPSG:4326",
|
|
|
|
|
+ });
|
|
|
|
|
+ featureList.push(...features);
|
|
|
|
|
+ });
|
|
|
|
|
+ return new VectorSource({
|
|
|
|
|
+ features: featureList,
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ createDecorStyle(feature) {
|
|
|
|
|
+ const decorType = feature.get("decorType");
|
|
|
|
|
+ if (decorType === "river") {
|
|
|
|
|
+ return [
|
|
|
|
|
+ new Style({
|
|
|
|
|
+ stroke: new Stroke({
|
|
|
|
|
+ color: "rgba(72, 171, 255, 0.16)",
|
|
|
|
|
+ width: 4,
|
|
|
|
|
+ }),
|
|
|
|
|
+ }),
|
|
|
|
|
+ new Style({
|
|
|
|
|
+ stroke: new Stroke({
|
|
|
|
|
+ color: "rgba(140, 220, 255, 0.42)",
|
|
|
|
|
+ width: 1.1,
|
|
|
|
|
+ }),
|
|
|
|
|
+ }),
|
|
|
|
|
+ ];
|
|
|
|
|
+ }
|
|
|
|
|
+ if (decorType === "rail") {
|
|
|
|
|
+ return new Style({
|
|
|
|
|
+ stroke: new Stroke({
|
|
|
|
|
+ color: "rgba(190, 220, 245, 0.16)",
|
|
|
|
|
+ width: 1,
|
|
|
|
|
+ lineDash: [4, 5],
|
|
|
|
|
+ }),
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ },
|
|
|
|
|
+ normalizeRegionName(name = "") {
|
|
|
|
|
+ return String(name)
|
|
|
|
|
+ .replace(/(特别行政区|自治区|壮族|回族|维吾尔|省|市)$/g, "")
|
|
|
|
|
+ .trim();
|
|
|
|
|
+ },
|
|
|
|
|
+ getProvinceName(feature) {
|
|
|
|
|
+ return feature?.getProperties?.().name || "";
|
|
|
|
|
+ },
|
|
|
|
|
+ findProvinceFeature(name) {
|
|
|
|
|
+ const target = this.normalizeRegionName(name);
|
|
|
|
|
+ if (!target) return null;
|
|
|
|
|
+ return (
|
|
|
|
|
+ this.provinceFeatures.find((feature) => {
|
|
|
|
|
+ const pname = this.normalizeRegionName(this.getProvinceName(feature));
|
|
|
|
|
+ return (
|
|
|
|
|
+ pname === target || pname.includes(target) || target.includes(pname)
|
|
|
|
|
+ );
|
|
|
|
|
+ }) || null
|
|
|
|
|
+ );
|
|
|
|
|
+ },
|
|
|
|
|
+ isProvinceActive(feature) {
|
|
|
|
|
+ const name = this.getProvinceName(feature);
|
|
|
|
|
+ const normalized = this.normalizeRegionName(name);
|
|
|
|
|
+ return (this.activeProvinceNames || []).some((item) => {
|
|
|
|
|
+ const n = this.normalizeRegionName(item);
|
|
|
|
|
+ return n === normalized || name === item;
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ isProvinceFocused(feature) {
|
|
|
|
|
+ const name = this.getProvinceName(feature);
|
|
|
|
|
+ if (!this.focusedProvinceName) return false;
|
|
|
|
|
+ return (
|
|
|
|
|
+ this.normalizeRegionName(name) ===
|
|
|
|
|
+ this.normalizeRegionName(this.focusedProvinceName)
|
|
|
|
|
+ );
|
|
|
|
|
+ },
|
|
|
|
|
+ shouldShowProvinceLabel(feature) {
|
|
|
|
|
+ // 默认全国各省都显示轻量省名;深缩放到市级时仅保留当前聚焦省
|
|
|
|
|
+ if (this.focusedProvinceName && this.currentZoom >= 6.4) {
|
|
|
|
|
+ return this.isProvinceFocused(feature);
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ },
|
|
|
|
|
+ /** MultiPolygon 只取最大面,避免海岛厚描边叠成光斑 */
|
|
|
|
|
+ getMainPolygonGeometry(feature) {
|
|
|
|
|
+ const geometry = feature.getGeometry();
|
|
|
|
|
+ if (!geometry) return null;
|
|
|
|
|
+ if (geometry.getType() !== "MultiPolygon") return geometry;
|
|
|
|
|
+ let largest = null;
|
|
|
|
|
+ let maxArea = 0;
|
|
|
|
|
+ geometry.getPolygons().forEach((polygon) => {
|
|
|
|
|
+ const area = Math.abs(polygon.getArea());
|
|
|
|
|
+ if (area > maxArea) {
|
|
|
|
|
+ maxArea = area;
|
|
|
|
|
+ largest = polygon;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ return largest || geometry;
|
|
|
|
|
+ },
|
|
|
|
|
+ createProvinceGlowStyle(feature) {
|
|
|
|
|
+ const active =
|
|
|
|
|
+ this.isProvinceActive(feature) || this.isProvinceFocused(feature);
|
|
|
|
|
+ const mainGeom = this.getMainPolygonGeometry(feature);
|
|
|
|
|
+ return [
|
|
|
|
|
+ new Style({
|
|
|
|
|
+ geometry: mainGeom || undefined,
|
|
|
|
|
+ stroke: new Stroke({
|
|
|
|
|
+ color: active
|
|
|
|
|
+ ? "rgba(255, 180, 60, 0.3)"
|
|
|
|
|
+ : "rgba(40, 190, 255, 0.26)",
|
|
|
|
|
+ width: active ? 14 : 12,
|
|
|
|
|
+ }),
|
|
|
|
|
+ }),
|
|
|
|
|
+ new Style({
|
|
|
|
|
+ geometry: mainGeom || undefined,
|
|
|
|
|
+ stroke: new Stroke({
|
|
|
|
|
+ color: active
|
|
|
|
|
+ ? "rgba(255, 210, 110, 0.48)"
|
|
|
|
|
+ : "rgba(90, 225, 255, 0.45)",
|
|
|
|
|
+ width: active ? 7 : 6,
|
|
|
|
|
+ }),
|
|
|
|
|
+ }),
|
|
|
|
|
+ ];
|
|
|
|
|
+ },
|
|
|
|
|
+ /** MultiPolygon 省(如浙江多岛)只取一个标注点,避免每个面各画一次省名 */
|
|
|
|
|
+ getProvinceLabelPoint(feature) {
|
|
|
|
|
+ const props = feature.getProperties() || {};
|
|
|
|
|
+ if (Array.isArray(props.cp) && props.cp.length >= 2) {
|
|
|
|
|
+ return new Point(props.cp);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (props.longitude != null && props.latitude != null) {
|
|
|
|
|
+ return new Point([Number(props.longitude), Number(props.latitude)]);
|
|
|
|
|
+ }
|
|
|
|
|
+ const geometry = feature.getGeometry();
|
|
|
|
|
+ if (!geometry) return null;
|
|
|
|
|
+ const type = geometry.getType();
|
|
|
|
|
+ if (type === "Polygon") {
|
|
|
|
|
+ return geometry.getInteriorPoint();
|
|
|
|
|
+ }
|
|
|
|
|
+ if (type === "MultiPolygon") {
|
|
|
|
|
+ const main = this.getMainPolygonGeometry(feature);
|
|
|
|
|
+ return main && main.getInteriorPoint ? main.getInteriorPoint() : null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ },
|
|
|
|
|
+ createProvinceStyle(feature) {
|
|
|
|
|
+ const props = feature.getProperties() || {};
|
|
|
|
|
+ const active =
|
|
|
|
|
+ this.isProvinceActive(feature) || this.isProvinceFocused(feature);
|
|
|
|
|
+ const showLabel = this.shouldShowProvinceLabel(feature);
|
|
|
|
|
+ const styles = [
|
|
|
|
|
+ new Style({
|
|
|
|
|
+ fill: new Fill({
|
|
|
|
|
+ color: active
|
|
|
|
|
+ ? "rgba(255, 168, 48, 0.58)"
|
|
|
|
|
+ : "rgba(52, 120, 220, 0.52)",
|
|
|
|
|
+ }),
|
|
|
|
|
+ stroke: new Stroke({
|
|
|
|
|
+ color: active ? "#ffe5a8" : "rgba(130, 235, 255, 0.98)",
|
|
|
|
|
+ width: active ? 2.4 : 1.35,
|
|
|
|
|
+ }),
|
|
|
|
|
+ }),
|
|
|
|
|
+ ];
|
|
|
|
|
+ if (showLabel) {
|
|
|
|
|
+ const labelPoint = this.getProvinceLabelPoint(feature);
|
|
|
|
|
+ styles.push(
|
|
|
|
|
+ new Style({
|
|
|
|
|
+ geometry: labelPoint || undefined,
|
|
|
|
|
+ text: new Text({
|
|
|
|
|
+ text: props.name || "",
|
|
|
|
|
+ font: active
|
|
|
|
|
+ ? "bold 13px Microsoft YaHei"
|
|
|
|
|
+ : "12px Microsoft YaHei",
|
|
|
|
|
+ overflow: true,
|
|
|
|
|
+ fill: new Fill({
|
|
|
|
|
+ color: active
|
|
|
|
|
+ ? "rgba(255, 236, 190, 0.98)"
|
|
|
|
|
+ : "rgba(214, 246, 255, 0.92)",
|
|
|
|
|
+ }),
|
|
|
|
|
+ stroke: new Stroke({
|
|
|
|
|
+ color: active
|
|
|
|
|
+ ? "rgba(90, 45, 0, 0.45)"
|
|
|
|
|
+ : "rgba(6, 36, 82, 0.55)",
|
|
|
|
|
+ width: 3,
|
|
|
|
|
+ }),
|
|
|
|
|
+ }),
|
|
|
|
|
+ }),
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ return styles;
|
|
|
|
|
+ },
|
|
|
|
|
+ createCountyBoundaryStyle() {
|
|
|
|
|
+ const zoom = this.currentZoom;
|
|
|
|
|
+ const alpha = zoom >= 5.5 ? 0.28 : zoom >= 4.5 ? 0.16 : 0.08;
|
|
|
|
|
+ return new Style({
|
|
|
|
|
+ stroke: new Stroke({
|
|
|
|
|
+ color: `rgba(150, 220, 255, ${alpha})`,
|
|
|
|
|
+ width: zoom >= 6 ? 0.7 : 0.5,
|
|
|
|
|
+ }),
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ createCityLabelStyle(feature) {
|
|
|
|
|
+ const name = feature.get("name") || "";
|
|
|
|
|
+ const focused = name && name === this.focusedCityName;
|
|
|
|
|
+ if (this.currentZoom < 5.6) return null;
|
|
|
|
|
+ return new Style({
|
|
|
|
|
+ text: new Text({
|
|
|
|
|
+ text: name,
|
|
|
|
|
+ font: focused ? "bold 12px Microsoft YaHei" : "11px Microsoft YaHei",
|
|
|
|
|
+ overflow: true,
|
|
|
|
|
+ fill: new Fill({
|
|
|
|
|
+ color: focused
|
|
|
|
|
+ ? "rgba(255, 236, 190, 0.98)"
|
|
|
|
|
+ : "rgba(210, 242, 255, 0.9)",
|
|
|
|
|
+ }),
|
|
|
|
|
+ stroke: new Stroke({
|
|
|
|
|
+ color: focused ? "rgba(90, 45, 0, 0.45)" : "rgba(6, 36, 82, 0.5)",
|
|
|
|
|
+ width: 2.5,
|
|
|
|
|
+ }),
|
|
|
|
|
+ }),
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ createMarkerStyle(feature) {
|
|
|
|
|
+ const data = feature.get("markerData") || feature.get("data") || {};
|
|
|
|
|
+ const val = data.val;
|
|
|
|
|
+ const iconSrc = this.getIconForValue(val);
|
|
|
|
|
+ const scale = val === "4" ? 0.12 : 0.08;
|
|
|
|
|
+ return new Style({
|
|
|
|
|
+ image: new Icon({
|
|
|
|
|
+ src: iconSrc,
|
|
|
|
|
+ scale,
|
|
|
|
|
+ anchor: [0.5, 1],
|
|
|
|
|
+ opacity: 1,
|
|
|
|
|
+ }),
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ getIconForValue(val) {
|
|
|
|
|
+ switch (val) {
|
|
|
|
|
+ case "-1":
|
|
|
|
|
+ case -1:
|
|
|
|
|
+ case "10":
|
|
|
|
|
+ case 10:
|
|
|
|
|
+ return icon01;
|
|
|
|
|
+ case 0:
|
|
|
|
|
+ case "0":
|
|
|
|
|
+ case 20:
|
|
|
|
|
+ case "20":
|
|
|
|
|
+ return icon02;
|
|
|
|
|
+ case 1:
|
|
|
|
|
+ case "1":
|
|
|
|
|
+ case 30:
|
|
|
|
|
+ case "30":
|
|
|
|
|
+ return icon03;
|
|
|
|
|
+ case "4":
|
|
|
|
|
+ return icon04;
|
|
|
|
|
+ case "5":
|
|
|
|
|
+ return icon05;
|
|
|
|
|
+ case "6":
|
|
|
|
|
+ return icon06;
|
|
|
|
|
+ default:
|
|
|
|
|
+ return defaultIcon;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ onHoverLeave() {
|
|
|
|
|
+ this.hovering = false;
|
|
|
|
|
+ if (this.hideTimer) clearTimeout(this.hideTimer);
|
|
|
|
|
+ this.hideTimer = setTimeout(() => {
|
|
|
|
|
+ if (!this.hovering) {
|
|
|
|
|
+ this.hoverInfo = null;
|
|
|
|
|
+ this.lastHoveredFeature = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 120);
|
|
|
|
|
+ },
|
|
|
|
|
+ fetchWindHoverInfo() {
|
|
|
|
|
+ if (!this.hoverInfo) return;
|
|
|
|
|
+ const param = {
|
|
|
|
|
+ batchcode: this.hoverInfo.batchCode,
|
|
|
|
|
+ fieldCode: this.hoverInfo.codeNumber,
|
|
|
|
|
+ };
|
|
|
|
|
+ queryFloatingWindowInfo(param)
|
|
|
|
|
+ .then((res) => {
|
|
|
|
|
+ this.newWind = res.data || {};
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(() => {
|
|
|
|
|
+ this.newWind = {};
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ calcHoverPosition(evt, hoverHeight = 280, hoverWidth = 240) {
|
|
|
|
|
+ const mapEl = this.map.getTargetElement();
|
|
|
|
|
+ const mapRect = mapEl.getBoundingClientRect();
|
|
|
|
|
+ const rootRect = this.$el.getBoundingClientRect();
|
|
|
|
|
+ const offset = 12;
|
|
|
|
|
+ let left =
|
|
|
|
|
+ mapRect.left - rootRect.left + evt.pixel[0] + offset;
|
|
|
|
|
+ let top =
|
|
|
|
|
+ mapRect.top - rootRect.top + evt.pixel[1] - hoverHeight / 2;
|
|
|
|
|
+ if (top + hoverHeight > rootRect.height - offset) {
|
|
|
|
|
+ top = mapRect.top - rootRect.top + evt.pixel[1] - hoverHeight - offset;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (top < offset) top = offset;
|
|
|
|
|
+ if (left + hoverWidth > rootRect.width - offset) {
|
|
|
|
|
+ left =
|
|
|
|
|
+ mapRect.left - rootRect.left + evt.pixel[0] - hoverWidth - offset;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (left < offset) left = offset;
|
|
|
|
|
+ return { top, left };
|
|
|
|
|
+ },
|
|
|
|
|
+ initPointerMove() {
|
|
|
|
|
+ if (!this.map) return;
|
|
|
|
|
+ this.map.on("pointermove", (evt) => {
|
|
|
|
|
+ if (evt.dragging) return;
|
|
|
|
|
+ const markerFeature = this.map.forEachFeatureAtPixel(
|
|
|
|
|
+ evt.pixel,
|
|
|
|
|
+ (hit) => hit,
|
|
|
|
|
+ {
|
|
|
|
|
+ hitTolerance: 8,
|
|
|
|
|
+ layerFilter: (layer) => layer === this.markerLayer,
|
|
|
|
|
+ },
|
|
|
|
|
+ );
|
|
|
|
|
+ const targetEl = this.map.getTargetElement();
|
|
|
|
|
+ if (markerFeature) {
|
|
|
|
|
+ const data =
|
|
|
|
|
+ markerFeature.get("markerData") || markerFeature.get("data");
|
|
|
|
|
+ if (!data) return;
|
|
|
|
|
+ targetEl.style.cursor = "pointer";
|
|
|
|
|
+ if (this.lastHoveredFeature !== markerFeature) {
|
|
|
|
|
+ this.lastHoveredFeature = markerFeature;
|
|
|
|
|
+ const pos = this.calcHoverPosition(evt);
|
|
|
|
|
+ this.hoverInfo = data;
|
|
|
|
|
+ this.hoverStyle = {
|
|
|
|
|
+ position: "absolute",
|
|
|
|
|
+ left: `${pos.left}px`,
|
|
|
|
|
+ top: `${pos.top}px`,
|
|
|
|
|
+ };
|
|
|
|
|
+ this.fetchWindHoverInfo();
|
|
|
|
|
+ } else if (this.hoverInfo) {
|
|
|
|
|
+ const pos = this.calcHoverPosition(evt);
|
|
|
|
|
+ this.hoverStyle = {
|
|
|
|
|
+ position: "absolute",
|
|
|
|
|
+ left: `${pos.left}px`,
|
|
|
|
|
+ top: `${pos.top}px`,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ targetEl.style.cursor = "";
|
|
|
|
|
+ if (!this.hovering) {
|
|
|
|
|
+ this.hoverInfo = null;
|
|
|
|
|
+ this.lastHoveredFeature = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ refreshProvinceStyle() {
|
|
|
|
|
+ if (this.provinceLayer) this.provinceLayer.changed();
|
|
|
|
|
+ if (this.provinceGlowLayer) this.provinceGlowLayer.changed();
|
|
|
|
|
+ if (this.countyLayer) this.countyLayer.changed();
|
|
|
|
|
+ },
|
|
|
|
|
+ loadCityLabels(provinceName) {
|
|
|
|
|
+ if (!this.cityLabelSource) return;
|
|
|
|
|
+ this.cityLabelSource.clear();
|
|
|
|
|
+ this.focusedCityName = "";
|
|
|
|
|
+ const key = CN_TO_PROVINCE_KEY[this.normalizeRegionName(provinceName)];
|
|
|
|
|
+ const collection = key ? ProvinceData[key] : null;
|
|
|
|
|
+ if (!collection || !collection.features) return;
|
|
|
|
|
+ const format = new GeoJSON();
|
|
|
|
|
+ const features = format.readFeatures(collection, {
|
|
|
|
|
+ dataProjection: "EPSG:4326",
|
|
|
|
|
+ featureProjection: "EPSG:4326",
|
|
|
|
|
+ });
|
|
|
|
|
+ features.forEach((feature) => {
|
|
|
|
|
+ const props = feature.getProperties() || {};
|
|
|
|
|
+ const cp = props.cp;
|
|
|
|
|
+ if (!Array.isArray(cp) || cp.length < 2) return;
|
|
|
|
|
+ const labelFeature = new Feature({
|
|
|
|
|
+ geometry: new Point(cp),
|
|
|
|
|
+ name: props.name || "",
|
|
|
|
|
+ });
|
|
|
|
|
+ this.cityLabelSource.addFeature(labelFeature);
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ clearCityLabels() {
|
|
|
|
|
+ if (this.cityLabelSource) this.cityLabelSource.clear();
|
|
|
|
|
+ this.focusedCityName = "";
|
|
|
|
|
+ },
|
|
|
|
|
+ fitFeature(feature, options = {}) {
|
|
|
|
|
+ if (!this.map || !feature) return;
|
|
|
|
|
+ const geometry = feature.getGeometry();
|
|
|
|
|
+ if (!geometry) return;
|
|
|
|
|
+ this.map.getView().fit(geometry.getExtent(), {
|
|
|
|
|
+ padding: options.padding || [56, 56, 56, 56],
|
|
|
|
|
+ duration: options.duration == null ? 700 : options.duration,
|
|
|
|
|
+ maxZoom: options.maxZoom || 7.4,
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ fitChina(animate = true) {
|
|
|
|
|
+ if (!this.map) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const view = this.map.getView();
|
|
|
|
|
+ const source = this.provinceLayer && this.provinceLayer.getSource();
|
|
|
|
|
+ if (source) {
|
|
|
|
|
+ const extent = source.getExtent();
|
|
|
|
|
+ view.fit(extent, {
|
|
|
|
|
+ padding: [12, 16, 28, 16],
|
|
|
|
|
+ duration: animate ? 500 : 0,
|
|
|
|
|
+ maxZoom: 6.4,
|
|
|
|
|
+ callback: () => {
|
|
|
|
|
+ // 宽屏 fit 后仍偏小,再略放大铺满中间区域
|
|
|
|
|
+ const z = view.getZoom() || DEFAULT_ZOOM;
|
|
|
|
|
+ const nextZoom = Math.min(z + 0.55, 6.4);
|
|
|
|
|
+ if (animate) {
|
|
|
|
|
+ view.animate({ zoom: nextZoom, duration: 280 });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ view.setZoom(nextZoom);
|
|
|
|
|
+ }
|
|
|
|
|
+ this.currentZoom = nextZoom;
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (animate) {
|
|
|
|
|
+ view.animate({
|
|
|
|
|
+ center: DEFAULT_CENTER,
|
|
|
|
|
+ zoom: DEFAULT_ZOOM,
|
|
|
|
|
+ duration: 600,
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ view.setCenter(DEFAULT_CENTER);
|
|
|
|
|
+ view.setZoom(DEFAULT_ZOOM);
|
|
|
|
|
+ }
|
|
|
|
|
+ this.currentZoom = DEFAULT_ZOOM;
|
|
|
|
|
+ },
|
|
|
|
|
+ fitMarkers(animate = true) {
|
|
|
|
|
+ if (!this.map || !this.markerSource) return;
|
|
|
|
|
+ const features = this.markerSource.getFeatures();
|
|
|
|
|
+ if (!features.length) {
|
|
|
|
|
+ this.fitChina(animate);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (features.length === 1) {
|
|
|
|
|
+ const coord = features[0].getGeometry().getCoordinates();
|
|
|
|
|
+ this.map.getView().animate({
|
|
|
|
|
+ center: coord,
|
|
|
|
|
+ zoom: 6.8,
|
|
|
|
|
+ duration: animate ? 700 : 0,
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const extent = this.markerSource.getExtent();
|
|
|
|
|
+ const spanLon = extent[2] - extent[0];
|
|
|
|
|
+ const spanLat = extent[3] - extent[1];
|
|
|
|
|
+ // 场站分布接近全国时,继续用放大后的全国视图,避免又缩回去
|
|
|
|
|
+ if (spanLon > 12 || spanLat > 10 || features.length > 6) {
|
|
|
|
|
+ this.fitChina(animate);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.map.getView().fit(extent, {
|
|
|
|
|
+ padding: [48, 56, 56, 56],
|
|
|
|
|
+ duration: animate ? 700 : 0,
|
|
|
|
|
+ maxZoom: 7,
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ focusProvince(name, options = {}) {
|
|
|
|
|
+ const feature = this.findProvinceFeature(name);
|
|
|
|
|
+ if (!feature) return false;
|
|
|
|
|
+ const provinceName = this.getProvinceName(feature);
|
|
|
|
|
+ this.focusedProvinceName = provinceName;
|
|
|
|
|
+ this.loadCityLabels(provinceName);
|
|
|
|
|
+ this.refreshProvinceStyle();
|
|
|
|
|
+ if (options.fit !== false) {
|
|
|
|
|
+ this.fitFeature(feature, {
|
|
|
|
|
+ maxZoom: options.maxZoom || 6.8,
|
|
|
|
|
+ duration: options.duration,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ },
|
|
|
|
|
+ focusCity(cityName, provinceName) {
|
|
|
|
|
+ if (provinceName) {
|
|
|
|
|
+ this.focusProvince(provinceName, { fit: false });
|
|
|
|
|
+ }
|
|
|
|
|
+ this.focusedCityName = cityName || "";
|
|
|
|
|
+ if (this.cityLabelLayer) this.cityLabelLayer.changed();
|
|
|
|
|
+ if (!this.cityLabelSource || !cityName) return false;
|
|
|
|
|
+ const feature = this.cityLabelSource
|
|
|
|
|
+ .getFeatures()
|
|
|
|
|
+ .find((item) => item.get("name") === cityName);
|
|
|
|
|
+ if (!feature) return false;
|
|
|
|
|
+ this.map.getView().animate({
|
|
|
|
|
+ center: feature.getGeometry().getCoordinates(),
|
|
|
|
|
+ zoom: Math.max(this.currentZoom, 7.2),
|
|
|
|
|
+ duration: 700,
|
|
|
|
|
+ });
|
|
|
|
|
+ return true;
|
|
|
|
|
+ },
|
|
|
|
|
+ initMapClick() {
|
|
|
|
|
+ if (!this.map) return;
|
|
|
|
|
+ this.map.on("singleclick", (evt) => {
|
|
|
|
|
+ const markerFeature = this.map.forEachFeatureAtPixel(
|
|
|
|
|
+ evt.pixel,
|
|
|
|
|
+ (hit) => hit,
|
|
|
|
|
+ {
|
|
|
|
|
+ hitTolerance: 8,
|
|
|
|
|
+ layerFilter: (layer) => layer === this.markerLayer,
|
|
|
|
|
+ },
|
|
|
|
|
+ );
|
|
|
|
|
+ if (markerFeature) {
|
|
|
|
|
+ const markerData =
|
|
|
|
|
+ markerFeature.get("markerData") || markerFeature.get("data");
|
|
|
|
|
+ if (markerData) {
|
|
|
|
|
+ this.$emit("feature-click", markerData);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ const feature = this.map.forEachFeatureAtPixel(
|
|
|
|
|
+ evt.pixel,
|
|
|
|
|
+ (hit) => hit,
|
|
|
|
|
+ {
|
|
|
|
|
+ layerFilter: (layer) =>
|
|
|
|
|
+ layer === this.provinceLayer || layer === this.cityLabelLayer,
|
|
|
|
|
+ },
|
|
|
|
|
+ );
|
|
|
|
|
+ if (!feature) return;
|
|
|
|
|
+ const cityName = feature.get("name");
|
|
|
|
|
+ if (feature.getGeometry() instanceof Point && cityName) {
|
|
|
|
|
+ this.focusedCityName = cityName;
|
|
|
|
|
+ if (this.cityLabelLayer) this.cityLabelLayer.changed();
|
|
|
|
|
+ this.$emit("city-click", {
|
|
|
|
|
+ name: cityName,
|
|
|
|
|
+ province: this.focusedProvinceName,
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const provinceName = this.getProvinceName(feature);
|
|
|
|
|
+ if (provinceName) {
|
|
|
|
|
+ this.focusProvince(provinceName);
|
|
|
|
|
+ this.$emit("province-click", {
|
|
|
|
|
+ name: provinceName,
|
|
|
|
|
+ center: feature.getProperties()?.cp || null,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ setMarkers(list = []) {
|
|
|
|
|
+ if (!this.markerSource) return;
|
|
|
|
|
+ this.markerSource.clear();
|
|
|
|
|
+ this.markerFeatures = [];
|
|
|
|
|
+ list.forEach((item) => this.addMarker(item));
|
|
|
|
|
+ },
|
|
|
|
|
+ addMarker(marker) {
|
|
|
|
|
+ if (!this.markerSource || !marker) return;
|
|
|
|
|
+ if (!Array.isArray(marker.point) || marker.point.length < 2) return;
|
|
|
|
|
+ const payload = {
|
|
|
|
|
+ ...marker,
|
|
|
|
|
+ val: marker.val != null ? marker.val : marker.analysisState,
|
|
|
|
|
+ };
|
|
|
|
|
+ const feature = new Feature({
|
|
|
|
|
+ geometry: new Point(marker.point),
|
|
|
|
|
+ });
|
|
|
|
|
+ feature.set("active", !!marker.active);
|
|
|
|
|
+ feature.set("markerData", payload);
|
|
|
|
|
+ feature.set("data", payload);
|
|
|
|
|
+ this.markerSource.addFeature(feature);
|
|
|
|
|
+ this.markerFeatures.push(feature);
|
|
|
|
|
+ },
|
|
|
|
|
+ clearMarkers() {
|
|
|
|
|
+ if (!this.markerSource) return;
|
|
|
|
|
+ this.markerSource.clear();
|
|
|
|
|
+ this.markerFeatures = [];
|
|
|
|
|
+ this.hoverInfo = null;
|
|
|
|
|
+ this.lastHoveredFeature = null;
|
|
|
|
|
+ },
|
|
|
|
|
+ moveAndZoom(data = { point: DEFAULT_CENTER, zoom: DEFAULT_ZOOM }) {
|
|
|
|
|
+ if (!this.map) return;
|
|
|
|
|
+ if (data.provinceName) {
|
|
|
|
|
+ this.focusProvince(data.provinceName, {
|
|
|
|
|
+ maxZoom: Number(data.zoom) || 6.8,
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (data.cityName) {
|
|
|
|
|
+ this.focusCity(data.cityName, data.provinceName);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const point = Array.isArray(data.point) ? data.point : DEFAULT_CENTER;
|
|
|
|
|
+ const zoom = Number(data.zoom) || DEFAULT_ZOOM;
|
|
|
|
|
+ this.map.getView().animate({
|
|
|
|
|
+ center: point,
|
|
|
|
|
+ zoom,
|
|
|
|
|
+ duration: 800,
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ zoomToChina() {
|
|
|
|
|
+ this.focusedProvinceName = "";
|
|
|
|
|
+ this.clearCityLabels();
|
|
|
|
|
+ this.refreshProvinceStyle();
|
|
|
|
|
+ this.fitChina(true);
|
|
|
|
|
+ },
|
|
|
|
|
+ zoomToProvince(name) {
|
|
|
|
|
+ return this.focusProvince(name);
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+};
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+.tech-blue-province-map {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ min-height: 420px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ border-radius: 14px;
|
|
|
|
|
+ background: radial-gradient(
|
|
|
|
|
+ circle at 50% 42%,
|
|
|
|
|
+ rgba(36, 93, 188, 0.34) 0%,
|
|
|
|
|
+ rgba(8, 22, 54, 0.12) 32%,
|
|
|
|
|
+ rgba(2, 9, 24, 0.98) 86%
|
|
|
|
|
+ ),
|
|
|
|
|
+ linear-gradient(180deg, #091730 0%, #020814 100%);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tech-blue-province-map__bg {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset: 0;
|
|
|
|
|
+ z-index: 0;
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+ background: radial-gradient(
|
|
|
|
|
+ circle at 50% 36%,
|
|
|
|
|
+ rgba(69, 131, 238, 0.2) 0%,
|
|
|
|
|
+ transparent 52%
|
|
|
|
|
+ ),
|
|
|
|
|
+ radial-gradient(
|
|
|
|
|
+ circle at 50% 72%,
|
|
|
|
|
+ rgba(10, 120, 255, 0.08) 0%,
|
|
|
|
|
+ transparent 40%
|
|
|
|
|
+ );
|
|
|
|
|
+ box-shadow: inset 0 0 180px rgba(0, 0, 0, 0.58),
|
|
|
|
|
+ inset 0 -90px 120px rgba(0, 0, 0, 0.62),
|
|
|
|
|
+ inset 0 24px 50px rgba(30, 137, 255, 0.08);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tech-blue-province-map__grid {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset: 0;
|
|
|
|
|
+ z-index: 1;
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+ background: linear-gradient(rgba(90, 182, 255, 0.05) 1px, transparent 1px),
|
|
|
|
|
+ linear-gradient(90deg, rgba(90, 182, 255, 0.04) 1px, transparent 1px),
|
|
|
|
|
+ linear-gradient(rgba(90, 182, 255, 0.02) 1px, transparent 1px),
|
|
|
|
|
+ linear-gradient(90deg, rgba(90, 182, 255, 0.02) 1px, transparent 1px);
|
|
|
|
|
+ background-size: 24px 24px, 24px 24px, 96px 96px, 96px 96px;
|
|
|
|
|
+ mix-blend-mode: screen;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tech-blue-province-map__stage {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ inset: 0;
|
|
|
|
|
+ z-index: 2;
|
|
|
|
|
+ transform: none;
|
|
|
|
|
+ transform-origin: 50% 50%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tech-blue-province-map__shadow {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ left: 12%;
|
|
|
|
|
+ right: 12%;
|
|
|
|
|
+ bottom: -2%;
|
|
|
|
|
+ height: 18%;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: radial-gradient(
|
|
|
|
|
+ ellipse at center,
|
|
|
|
|
+ rgba(0, 180, 255, 0.28) 0%,
|
|
|
|
|
+ rgba(0, 50, 110, 0.2) 40%,
|
|
|
|
|
+ transparent 74%
|
|
|
|
|
+ );
|
|
|
|
|
+ filter: blur(12px);
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+ z-index: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tech-blue-province-map__canvas {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ z-index: 1;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ filter: drop-shadow(0 0 18px rgba(40, 190, 255, 0.35))
|
|
|
|
|
+ drop-shadow(0 16px 28px rgba(0, 0, 0, 0.5));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tech-blue-province-map__canvas ::v-deep .ol-zoom {
|
|
|
|
|
+ top: 16px;
|
|
|
|
|
+ left: auto;
|
|
|
|
|
+ right: 16px;
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tech-blue-province-map__canvas ::v-deep .ol-zoom button {
|
|
|
|
|
+ width: 28px;
|
|
|
|
|
+ height: 28px;
|
|
|
|
|
+ margin: 3px 0;
|
|
|
|
|
+ color: #9fe9ff;
|
|
|
|
|
+ background: rgba(6, 24, 52, 0.88);
|
|
|
|
|
+ border: 1px solid rgba(70, 190, 255, 0.55);
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ box-shadow: 0 0 10px rgba(40, 170, 255, 0.25);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tech-blue-province-map__canvas ::v-deep .ol-zoom button:hover {
|
|
|
|
|
+ background: rgba(18, 56, 110, 0.95);
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tech-blue-province-map__south-sea {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: 28px;
|
|
|
|
|
+ bottom: 24px;
|
|
|
|
|
+ z-index: 4;
|
|
|
|
|
+ width: 148px;
|
|
|
|
|
+ height: 108px;
|
|
|
|
|
+ border: 1px solid rgba(90, 220, 255, 0.75);
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ background: linear-gradient(
|
|
|
|
|
+ 180deg,
|
|
|
|
|
+ rgba(8, 28, 62, 0.92) 0%,
|
|
|
|
|
+ rgba(3, 12, 32, 0.96) 100%
|
|
|
|
|
+ );
|
|
|
|
|
+ box-shadow: 0 0 16px rgba(40, 180, 255, 0.28),
|
|
|
|
|
+ inset 0 0 18px rgba(30, 140, 255, 0.16);
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tech-blue-province-map__south-sea-title {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ left: 8px;
|
|
|
|
|
+ top: 6px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ color: rgba(190, 240, 255, 0.92);
|
|
|
|
|
+ text-shadow: 0 0 6px rgba(60, 200, 255, 0.55);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tech-blue-province-map__south-sea-dot {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ width: 4px;
|
|
|
|
|
+ height: 4px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: #8beeff;
|
|
|
|
|
+ box-shadow: 0 0 6px rgba(100, 220, 255, 0.9);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tech-blue-province-map__hover {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ z-index: 20;
|
|
|
|
|
+ width: 248px;
|
|
|
|
|
+ pointer-events: auto;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ border: 1px solid rgba(90, 210, 255, 0.55);
|
|
|
|
|
+ box-shadow:
|
|
|
|
|
+ 0 0 20px rgba(40, 160, 255, 0.28),
|
|
|
|
|
+ 0 12px 28px rgba(0, 0, 0, 0.45);
|
|
|
|
|
+ backdrop-filter: blur(6px);
|
|
|
|
|
+
|
|
|
|
|
+ h3 {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ padding: 8px 12px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #eaf9ff;
|
|
|
|
|
+ background: linear-gradient(
|
|
|
|
|
+ 90deg,
|
|
|
|
|
+ rgba(30, 110, 190, 0.9) 0%,
|
|
|
|
|
+ rgba(12, 48, 100, 0.92) 100%
|
|
|
|
|
+ );
|
|
|
|
|
+ border-bottom: 1px solid rgba(90, 210, 255, 0.35);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tech-blue-province-map__hover-body {
|
|
|
|
|
+ padding: 4px 0;
|
|
|
|
|
+ background: rgba(4, 18, 42, 0.94);
|
|
|
|
|
+
|
|
|
|
|
+ p {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ padding: 5px 12px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: rgba(210, 240, 255, 0.88);
|
|
|
|
|
+
|
|
|
|
|
+ span:last-child {
|
|
|
|
|
+ color: #9fe9ff;
|
|
|
|
|
+ text-align: right;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|