You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1375 lines
40 KiB
1375 lines
40 KiB
<template>
|
|
<div class="yak-ol-map">
|
|
<div :ref="setMapElement" class="yak-ol-map__canvas"></div>
|
|
<div
|
|
v-if="coordinateCopyTip.visible"
|
|
class="yak-ol-map__copy-tip"
|
|
:class="{ 'is-error': coordinateCopyTip.error }"
|
|
:style="{ left: `${coordinateCopyTip.x}px`, top: `${coordinateCopyTip.y}px` }"
|
|
>
|
|
{{ coordinateCopyTip.text }}
|
|
</div>
|
|
<div
|
|
v-if="coordinateCopyMenu.visible"
|
|
class="yak-ol-map__copy-menu"
|
|
:class="{ 'is-error': coordinateCopyMenu.error }"
|
|
:style="{ left: `${coordinateCopyMenu.x}px`, top: `${coordinateCopyMenu.y}px` }"
|
|
@click.stop
|
|
>
|
|
<span>{{ coordinateCopyMenu.text }}</span>
|
|
<button type="button" @click="handleCoordinateMenuCopy">复制</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import "ol/ol.css"
|
|
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue"
|
|
import Map from "ol/Map.js"
|
|
import View from "ol/View.js"
|
|
import Feature from "ol/Feature.js"
|
|
import GeoJSON from "ol/format/GeoJSON.js"
|
|
import ImageLayer from "ol/layer/Image.js"
|
|
import TileLayer from "ol/layer/Tile.js"
|
|
import VectorLayer from "ol/layer/Vector.js"
|
|
import Point from "ol/geom/Point.js"
|
|
import ImageWMS from "ol/source/ImageWMS.js"
|
|
import TileWMS from "ol/source/TileWMS.js"
|
|
import XYZ from "ol/source/XYZ.js"
|
|
import VectorSource from "ol/source/Vector.js"
|
|
import { bbox as bboxStrategy } from "ol/loadingstrategy.js"
|
|
import { defaults as defaultControls } from "ol/control/defaults.js"
|
|
import { unByKey } from "ol/Observable.js"
|
|
import { fromLonLat, toLonLat } from "ol/proj.js"
|
|
import CircleStyle from "ol/style/Circle.js"
|
|
import Fill from "ol/style/Fill.js"
|
|
import RegularShape from "ol/style/RegularShape.js"
|
|
import Stroke from "ol/style/Stroke.js"
|
|
import Style from "ol/style/Style.js"
|
|
import Text from "ol/style/Text.js"
|
|
import { hongyuanTownshipLabelPoints, hongyuanTownshipsGeoJson } from "@/config/townshipBoundaries"
|
|
|
|
const props = defineProps({
|
|
active: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
tileUrlTemplate: {
|
|
type: String,
|
|
default: "https://qkl-map.oss-cn-chengdu.aliyuncs.com/hy-result/{z}/{x}/{y}.png",
|
|
},
|
|
center: {
|
|
type: Array,
|
|
default: () => [103.07984, 33.029058],
|
|
},
|
|
minZoom: {
|
|
type: Number,
|
|
default: 3,
|
|
},
|
|
maxZoom: {
|
|
type: Number,
|
|
default: 21,
|
|
},
|
|
initialZoom: {
|
|
type: Number,
|
|
default: 21,
|
|
},
|
|
overlayLayer: {
|
|
type: Object,
|
|
default: () => ({
|
|
sourceType: "wms",
|
|
layerName: "ne:daping_yak_marks",
|
|
styles: "yak_marks",
|
|
cqlFilter: "gov_show = true",
|
|
name: "牦牛识别标记",
|
|
color: "#FFFF00",
|
|
opacity: 0.96,
|
|
}),
|
|
},
|
|
overlayLayers: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
})
|
|
|
|
const emit = defineEmits(["ready", "tile-status", "layer-status", "map-click"])
|
|
|
|
const HONGYUAN_CENTER = [103.07984, 33.029058]
|
|
const INITIAL_ZOOM = 21
|
|
const TOWN_BOUNDARY_GLOW_Z_INDEX = 232
|
|
const TOWN_BOUNDARY_LINE_Z_INDEX = 233
|
|
const TOWN_LABEL_LAYER_Z_INDEX = 240
|
|
const boundaryFormat = new GeoJSON()
|
|
const boundaryGlowStyle = new Style({
|
|
stroke: new Stroke({
|
|
color: "rgba(1, 18, 27, 0.72)",
|
|
width: 3.4,
|
|
}),
|
|
})
|
|
const boundaryLineStyle = new Style({
|
|
fill: new Fill({
|
|
color: "rgba(3, 18, 31, 0)",
|
|
}),
|
|
stroke: new Stroke({
|
|
color: "rgba(126, 251, 246, 0.9)",
|
|
width: 1.15,
|
|
}),
|
|
})
|
|
const pastureFocusStyle = [
|
|
new Style({
|
|
fill: new Fill({ color: "rgba(255, 76, 96, 0)" }),
|
|
stroke: new Stroke({ color: "rgba(255, 76, 96, 0.4)", width: 8 }),
|
|
}),
|
|
new Style({
|
|
stroke: new Stroke({ color: "rgba(255, 48, 68, 0.98)", width: 2.8 }),
|
|
}),
|
|
]
|
|
const pastureFocusYakStyle = [
|
|
new Style({
|
|
fill: new Fill({ color: "rgba(255, 255, 0, 0)" }),
|
|
stroke: new Stroke({ color: "rgba(38, 50, 0, 0.9)", width: 3 }),
|
|
}),
|
|
new Style({
|
|
fill: new Fill({ color: "rgba(255, 255, 0, 0)" }),
|
|
stroke: new Stroke({ color: "rgba(255, 255, 0, 1)", width: 1.4 }),
|
|
}),
|
|
new Style({
|
|
image: new CircleStyle({
|
|
radius: 3,
|
|
fill: new Fill({ color: "rgba(255, 255, 0, 0)" }),
|
|
stroke: new Stroke({ color: "rgba(255, 255, 0, 1)", width: 1.4 }),
|
|
}),
|
|
}),
|
|
]
|
|
const labelPointStyle = new Style({
|
|
image: new CircleStyle({
|
|
radius: 2.4,
|
|
fill: new Fill({ color: "rgba(234, 255, 255, 0.92)" }),
|
|
stroke: new Stroke({ color: "rgba(48, 220, 255, 0.9)", width: 1 }),
|
|
}),
|
|
})
|
|
|
|
const mapEl = ref(null)
|
|
let olMap = null
|
|
let baseSource = null
|
|
let overlaySource = null
|
|
let overlaySources = []
|
|
let overlayMapLayer = null
|
|
let overlayMapLayers = []
|
|
let pastureFocusSource = null
|
|
let pastureFocusLayer = null
|
|
let pastureFocusYakSource = null
|
|
let pastureFocusYakLayer = null
|
|
let pastureFocusYakRequestId = 0
|
|
let contextMenuHandler = null
|
|
let rightPointerDownHandler = null
|
|
let coordinateTipTimer = null
|
|
let lastRightClickCopyTime = 0
|
|
let eventKeys = []
|
|
let wmsEventKeys = []
|
|
const coordinateCopyTip = ref({
|
|
visible: false,
|
|
text: "",
|
|
x: 0,
|
|
y: 0,
|
|
error: false,
|
|
})
|
|
const coordinateCopyMenu = ref({
|
|
visible: false,
|
|
text: "",
|
|
x: 0,
|
|
y: 0,
|
|
error: false,
|
|
})
|
|
const pendingTiles = new Set()
|
|
const tileState = {
|
|
requested: 0,
|
|
loaded: 0,
|
|
failed: 0,
|
|
zoom: INITIAL_ZOOM,
|
|
}
|
|
|
|
function setMapElement(el) {
|
|
mapEl.value = el || null
|
|
if (el) initMap()
|
|
}
|
|
|
|
function tileCoordKey(event) {
|
|
const coord = event?.tile?.getTileCoord?.()
|
|
return Array.isArray(coord) ? coord.join("/") : ""
|
|
}
|
|
|
|
function currentZoom() {
|
|
const zoom = Number(olMap?.getView?.()?.getZoom?.())
|
|
return Number.isFinite(zoom) ? Math.round(zoom) : normalizeInitialZoom()
|
|
}
|
|
|
|
function normalizeCenter(center = props.center) {
|
|
const [lng, lat] = Array.isArray(center) ? center : []
|
|
const nextLng = Number(lng)
|
|
const nextLat = Number(lat)
|
|
return Number.isFinite(nextLng) && Number.isFinite(nextLat) ? [nextLng, nextLat] : HONGYUAN_CENTER
|
|
}
|
|
|
|
function locateMap(center = props.center) {
|
|
const view = olMap?.getView?.()
|
|
if (!view) return
|
|
const zoom = normalizeInitialZoom()
|
|
resetTileState(zoom)
|
|
view.setCenter(fromLonLat(normalizeCenter(center)))
|
|
view.setZoom(zoom)
|
|
window.setTimeout(() => olMap?.updateSize?.(), 0)
|
|
}
|
|
|
|
function normalizeInitialZoom() {
|
|
const zoom = Number(props.initialZoom)
|
|
const minZoom = Number(props.minZoom)
|
|
const maxZoom = Number(props.maxZoom)
|
|
const safeZoom = Number.isFinite(zoom) ? zoom : INITIAL_ZOOM
|
|
return Math.min(Number.isFinite(maxZoom) ? maxZoom : INITIAL_ZOOM, Math.max(Number.isFinite(minZoom) ? minZoom : 0, safeZoom))
|
|
}
|
|
|
|
function resetTileState(zoom = currentZoom()) {
|
|
pendingTiles.clear()
|
|
tileState.requested = 0
|
|
tileState.loaded = 0
|
|
tileState.failed = 0
|
|
tileState.zoom = zoom
|
|
}
|
|
|
|
function emitTileStatus(type, text) {
|
|
emit("tile-status", {
|
|
type,
|
|
text,
|
|
loaded: tileState.loaded,
|
|
failed: tileState.failed,
|
|
total: tileState.requested,
|
|
zoom: tileState.zoom,
|
|
tileUrlTemplate: props.tileUrlTemplate,
|
|
})
|
|
}
|
|
|
|
function emitLayerStatus(type, text) {
|
|
emit("layer-status", {
|
|
type,
|
|
text,
|
|
})
|
|
}
|
|
|
|
function buildBaseSource() {
|
|
baseSource = new XYZ({
|
|
url: props.tileUrlTemplate,
|
|
minZoom: props.minZoom,
|
|
maxZoom: props.maxZoom,
|
|
crossOrigin: "anonymous",
|
|
interpolate: true,
|
|
transition: 0,
|
|
wrapX: false,
|
|
})
|
|
|
|
eventKeys.push(
|
|
baseSource.on("tileloadstart", (event) => {
|
|
const key = tileCoordKey(event)
|
|
if (key && pendingTiles.has(key)) return
|
|
if (key) pendingTiles.add(key)
|
|
tileState.requested += 1
|
|
tileState.zoom = currentZoom()
|
|
emitTileStatus("loading", `正在加载 hy-result ${tileState.zoom}级影像底图`)
|
|
}),
|
|
)
|
|
eventKeys.push(
|
|
baseSource.on("tileloadend", (event) => {
|
|
const key = tileCoordKey(event)
|
|
if (key && !pendingTiles.has(key)) return
|
|
if (key) pendingTiles.delete(key)
|
|
tileState.loaded += 1
|
|
tileState.zoom = currentZoom()
|
|
const done = pendingTiles.size === 0
|
|
emitTileStatus(done ? "success" : "loading", done ? "hy-result 影像底图已加载" : `正在加载 hy-result ${tileState.zoom}级影像底图`)
|
|
}),
|
|
)
|
|
eventKeys.push(
|
|
baseSource.on("tileloaderror", (event) => {
|
|
const key = tileCoordKey(event)
|
|
if (key && !pendingTiles.has(key)) return
|
|
if (key) pendingTiles.delete(key)
|
|
tileState.failed += 1
|
|
tileState.zoom = currentZoom()
|
|
const type = tileState.loaded > 0 ? "warning" : "error"
|
|
emitTileStatus(type, type === "warning" ? "hy-result 影像底图覆盖不足" : "hy-result 影像底图加载失败")
|
|
}),
|
|
)
|
|
|
|
return baseSource
|
|
}
|
|
|
|
function normalizeOverlayLayer(layer = props.overlayLayer) {
|
|
if (!layer || layer.visible === false || !layer.layerName) return null
|
|
const sourceType = layer.sourceType || "wms"
|
|
return {
|
|
sourceType,
|
|
url: layer.url || (sourceType === "wfs" ? "/geoserver/ne/wfs" : "/geoserver/ne/wms"),
|
|
renderMode: layer.renderMode || "tile",
|
|
layerName: layer.layerName,
|
|
styles: layer.styles || "",
|
|
sldBody: layer.sldBody || "",
|
|
cqlFilter: layer.cqlFilter || "",
|
|
name: layer.name || "业务图层",
|
|
color: layer.color || layer.strokeColor || layer.fillColor || "#FFFF00",
|
|
fillColor: layer.fillColor || layer.color || "rgba(255, 227, 90, 0.05)",
|
|
strokeColor: layer.strokeColor || layer.color || "#FFFF00",
|
|
glowColor: layer.glowColor || "rgba(255, 227, 90, 0.3)",
|
|
opacity: Number.isFinite(Number(layer.opacity)) ? Number(layer.opacity) : 0.96,
|
|
lineWidth: Number.isFinite(Number(layer.lineWidth)) ? Number(layer.lineWidth) : 2.6,
|
|
zIndex: Number.isFinite(Number(layer.zIndex)) ? Number(layer.zIndex) : 20,
|
|
maxFeatures: layer.maxFeatures || 12000,
|
|
geometryField: layer.geometryField || "geom",
|
|
loadingStrategy: layer.loadingStrategy || "",
|
|
propertyName: layer.propertyName || (layer.boundaryOnly ? [layer.geometryField || "geom"] : undefined),
|
|
bboxPadding: Number.isFinite(Number(layer.bboxPadding)) ? Number(layer.bboxPadding) : 0.003,
|
|
}
|
|
}
|
|
|
|
function normalizeOverlayLayers() {
|
|
const layers = Array.isArray(props.overlayLayers) ? props.overlayLayers.filter(Boolean) : []
|
|
const sourceLayers = layers.length ? layers : [props.overlayLayer].filter(Boolean)
|
|
return sourceLayers.map((layer) => normalizeOverlayLayer(layer)).filter(Boolean)
|
|
}
|
|
|
|
function registerOverlaySource(source) {
|
|
if (!source) return source
|
|
overlaySource = source
|
|
overlaySources.push(source)
|
|
return source
|
|
}
|
|
|
|
function isActiveOverlaySource(source) {
|
|
return Boolean(source && overlaySources.includes(source))
|
|
}
|
|
|
|
function buildOverlayWmsParams(layer) {
|
|
const params = {
|
|
SERVICE: "WMS",
|
|
VERSION: "1.1.1",
|
|
LAYERS: layer.layerName,
|
|
FORMAT: "image/png",
|
|
TRANSPARENT: true,
|
|
TILED: true,
|
|
}
|
|
if (layer.styles) params.STYLES = layer.styles
|
|
if (layer.cqlFilter) params.CQL_FILTER = layer.cqlFilter
|
|
if (layer.sldBody) params.SLD_BODY = layer.sldBody
|
|
return params
|
|
}
|
|
|
|
function bindOverlayImageEvents(source, layer) {
|
|
wmsEventKeys.push(source.on("imageloadstart", () => emitLayerStatus("loading", `正在加载${layer.name}`)))
|
|
wmsEventKeys.push(source.on("imageloadend", () => emitLayerStatus("success", `${layer.name}已加载`)))
|
|
wmsEventKeys.push(source.on("imageloaderror", () => emitLayerStatus("error", `${layer.name}加载失败`)))
|
|
}
|
|
|
|
function bindOverlayTileEvents(source, layer) {
|
|
wmsEventKeys.push(source.on("tileloadstart", () => emitLayerStatus("loading", `正在加载${layer.name}`)))
|
|
wmsEventKeys.push(source.on("tileloadend", () => emitLayerStatus("success", `${layer.name}已加载`)))
|
|
wmsEventKeys.push(source.on("tileloaderror", () => emitLayerStatus("error", `${layer.name}加载失败`)))
|
|
}
|
|
|
|
function buildOverlayImageWmsSource(layer) {
|
|
const source = new ImageWMS({
|
|
url: layer.url,
|
|
params: buildOverlayWmsParams(layer),
|
|
serverType: "geoserver",
|
|
crossOrigin: "anonymous",
|
|
ratio: 1,
|
|
})
|
|
|
|
bindOverlayImageEvents(source, layer)
|
|
return source
|
|
}
|
|
|
|
function buildOverlayWmsSource(layer) {
|
|
const source = new TileWMS({
|
|
url: layer.url,
|
|
params: buildOverlayWmsParams(layer),
|
|
serverType: "geoserver",
|
|
crossOrigin: "anonymous",
|
|
transition: 0,
|
|
})
|
|
|
|
bindOverlayTileEvents(source, layer)
|
|
return source
|
|
}
|
|
|
|
function combineCqlFilters(...filters) {
|
|
const clauses = filters.map((item) => String(item || "").trim()).filter(Boolean)
|
|
if (!clauses.length) return ""
|
|
return clauses.length === 1 ? clauses[0] : clauses.map((item) => `(${item})`).join(" AND ")
|
|
}
|
|
|
|
function buildOverlayWfsUrl(layer, options = {}) {
|
|
const params = new URLSearchParams({
|
|
service: "WFS",
|
|
version: "1.0.0",
|
|
request: "GetFeature",
|
|
typeName: layer.layerName,
|
|
outputFormat: "json",
|
|
maxFeatures: String(layer.maxFeatures),
|
|
})
|
|
const propertyName = options.propertyName ?? layer.propertyName
|
|
if (Array.isArray(propertyName) && propertyName.length) params.set("propertyName", propertyName.join(","))
|
|
else if (propertyName) params.set("propertyName", propertyName)
|
|
const cqlFilter = combineCqlFilters(layer.cqlFilter, options.cqlFilter)
|
|
if (cqlFilter) params.set("CQL_FILTER", cqlFilter)
|
|
return `${layer.url}?${params.toString()}`
|
|
}
|
|
|
|
function createBboxCqlFilter(layer, extent) {
|
|
const bottomLeft = toLonLat([extent[0], extent[1]])
|
|
const topRight = toLonLat([extent[2], extent[3]])
|
|
const padding = layer.bboxPadding
|
|
const minLng = Math.min(bottomLeft[0], topRight[0]) - padding
|
|
const minLat = Math.min(bottomLeft[1], topRight[1]) - padding
|
|
const maxLng = Math.max(bottomLeft[0], topRight[0]) + padding
|
|
const maxLat = Math.max(bottomLeft[1], topRight[1]) + padding
|
|
return `BBOX(${layer.geometryField},${minLng},${minLat},${maxLng},${maxLat})`
|
|
}
|
|
|
|
function buildOverlayBboxVectorSource(layer) {
|
|
const source = new VectorSource({
|
|
wrapX: false,
|
|
strategy: bboxStrategy,
|
|
})
|
|
const format = new GeoJSON()
|
|
const loadedFeatureIds = new Set()
|
|
source.setLoader((extent, resolution, projection, success, failure) => {
|
|
emitLayerStatus("loading", `正在加载${layer.name}`)
|
|
fetch(buildOverlayWfsUrl(layer, { cqlFilter: createBboxCqlFilter(layer, extent) }), { credentials: "include" })
|
|
.then((response) => {
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
return response.json()
|
|
})
|
|
.then((geojson) => {
|
|
if (!isActiveOverlaySource(source)) return
|
|
const features = format.readFeatures(geojson, {
|
|
dataProjection: "EPSG:4326",
|
|
featureProjection: projection,
|
|
})
|
|
const nextFeatures = features.filter((feature) => {
|
|
const id = feature.getId?.()
|
|
if (!id || loadedFeatureIds.has(id)) return false
|
|
loadedFeatureIds.add(id)
|
|
return true
|
|
})
|
|
source.addFeatures(nextFeatures)
|
|
success?.(nextFeatures)
|
|
emitLayerStatus(features.length ? "success" : "warning", features.length ? `${layer.name}已加载` : `${layer.name}暂无数据`)
|
|
})
|
|
.catch(() => {
|
|
if (!isActiveOverlaySource(source)) return
|
|
source.removeLoadedExtent?.(extent)
|
|
failure?.()
|
|
emitLayerStatus("error", `${layer.name}加载失败`)
|
|
})
|
|
})
|
|
return source
|
|
}
|
|
|
|
function buildOverlayVectorSource(layer) {
|
|
if (layer.loadingStrategy === "bbox") return buildOverlayBboxVectorSource(layer)
|
|
const source = new VectorSource({ wrapX: false })
|
|
const format = new GeoJSON()
|
|
emitLayerStatus("loading", `正在加载${layer.name}`)
|
|
fetch(buildOverlayWfsUrl(layer), { credentials: "include" })
|
|
.then((response) => {
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
return response.json()
|
|
})
|
|
.then((geojson) => {
|
|
if (!isActiveOverlaySource(source)) return
|
|
const features = format.readFeatures(geojson, {
|
|
dataProjection: "EPSG:4326",
|
|
featureProjection: "EPSG:3857",
|
|
})
|
|
source.clear(true)
|
|
source.addFeatures(features)
|
|
emitLayerStatus(features.length ? "success" : "warning", features.length ? `${layer.name}已加载` : `${layer.name}暂无数据`)
|
|
})
|
|
.catch(() => {
|
|
if (!isActiveOverlaySource(source)) return
|
|
emitLayerStatus("error", `${layer.name}加载失败`)
|
|
})
|
|
return source
|
|
}
|
|
|
|
function createOverlayVectorStyle(layer) {
|
|
const lineWidth = layer.lineWidth
|
|
return [
|
|
new Style({
|
|
fill: new Fill({ color: layer.fillColor }),
|
|
stroke: new Stroke({ color: layer.glowColor, width: lineWidth + 5 }),
|
|
}),
|
|
new Style({
|
|
stroke: new Stroke({ color: layer.strokeColor, width: lineWidth }),
|
|
}),
|
|
]
|
|
}
|
|
|
|
function createOverlayMapLayer(layerConfig = props.overlayLayer) {
|
|
const layer = normalizeOverlayLayer(layerConfig)
|
|
if (!layer) return null
|
|
if (layer.sourceType === "wfs") {
|
|
const source = registerOverlaySource(buildOverlayVectorSource(layer))
|
|
const overlayVectorStyle = createOverlayVectorStyle(layer)
|
|
return new VectorLayer({
|
|
source,
|
|
style: () => overlayVectorStyle,
|
|
zIndex: layer.zIndex,
|
|
renderBuffer: 160,
|
|
updateWhileAnimating: true,
|
|
updateWhileInteracting: true,
|
|
})
|
|
}
|
|
if (layer.renderMode === "image") {
|
|
const source = registerOverlaySource(buildOverlayImageWmsSource(layer))
|
|
return new ImageLayer({
|
|
source,
|
|
opacity: layer.opacity,
|
|
zIndex: layer.zIndex,
|
|
})
|
|
}
|
|
const source = registerOverlaySource(buildOverlayWmsSource(layer))
|
|
return new TileLayer({
|
|
source,
|
|
opacity: layer.opacity,
|
|
zIndex: layer.zIndex,
|
|
})
|
|
}
|
|
|
|
function refreshOverlayLayer() {
|
|
if (!olMap) return
|
|
const staleLayers = new Set([...overlayMapLayers, overlayMapLayer].filter(Boolean))
|
|
staleLayers.forEach((layer) => olMap.removeLayer(layer))
|
|
overlayMapLayers = []
|
|
overlayMapLayer = null
|
|
if (wmsEventKeys.length) {
|
|
unByKey(wmsEventKeys)
|
|
wmsEventKeys = []
|
|
}
|
|
overlaySource = null
|
|
overlaySources = []
|
|
overlayMapLayers = normalizeOverlayLayers().map((layer) => createOverlayMapLayer(layer)).filter(Boolean)
|
|
overlayMapLayer = overlayMapLayers[0] || null
|
|
overlayMapLayers.forEach((layer) => olMap.addLayer(layer))
|
|
}
|
|
|
|
function createBoundaryLayers() {
|
|
const boundaryFeatures = boundaryFormat.readFeatures(hongyuanTownshipsGeoJson, {
|
|
dataProjection: "EPSG:4326",
|
|
featureProjection: "EPSG:3857",
|
|
})
|
|
const boundarySource = new VectorSource({
|
|
features: boundaryFeatures,
|
|
})
|
|
|
|
const labelFeatures = hongyuanTownshipLabelPoints
|
|
.filter((item) => item.name && Array.isArray(item.center))
|
|
.map((item) => {
|
|
const feature = new Feature({
|
|
geometry: new Point(fromLonLat(item.center)),
|
|
name: item.name,
|
|
})
|
|
feature.setId(`yak-town-label-${item.name}`)
|
|
return feature
|
|
})
|
|
const labelSource = new VectorSource({
|
|
features: labelFeatures,
|
|
})
|
|
|
|
return [
|
|
new VectorLayer({
|
|
source: boundarySource,
|
|
style: () => boundaryGlowStyle,
|
|
zIndex: TOWN_BOUNDARY_GLOW_Z_INDEX,
|
|
renderBuffer: 80,
|
|
updateWhileAnimating: true,
|
|
updateWhileInteracting: true,
|
|
}),
|
|
new VectorLayer({
|
|
source: boundarySource,
|
|
style: () => boundaryLineStyle,
|
|
zIndex: TOWN_BOUNDARY_LINE_Z_INDEX,
|
|
renderBuffer: 80,
|
|
updateWhileAnimating: true,
|
|
updateWhileInteracting: true,
|
|
}),
|
|
new VectorLayer({
|
|
source: labelSource,
|
|
declutter: true,
|
|
zIndex: TOWN_LABEL_LAYER_Z_INDEX,
|
|
renderBuffer: 120,
|
|
updateWhileAnimating: true,
|
|
updateWhileInteracting: true,
|
|
style: createTownLabelStyle,
|
|
}),
|
|
]
|
|
}
|
|
|
|
function createPastureFocusLayer() {
|
|
pastureFocusSource = new VectorSource({ wrapX: false })
|
|
pastureFocusLayer = new VectorLayer({
|
|
source: pastureFocusSource,
|
|
style: () => pastureFocusStyle,
|
|
zIndex: 180,
|
|
renderBuffer: 160,
|
|
updateWhileAnimating: true,
|
|
updateWhileInteracting: true,
|
|
})
|
|
return pastureFocusLayer
|
|
}
|
|
|
|
function createPastureFocusYakLayer() {
|
|
pastureFocusYakSource = new VectorSource({ wrapX: false })
|
|
pastureFocusYakLayer = new VectorLayer({
|
|
source: pastureFocusYakSource,
|
|
style: () => pastureFocusYakStyle,
|
|
zIndex: 190,
|
|
renderBuffer: 160,
|
|
updateWhileAnimating: true,
|
|
updateWhileInteracting: true,
|
|
})
|
|
return pastureFocusYakLayer
|
|
}
|
|
|
|
function normalizePastureGrasslandRows(value) {
|
|
if (!value) return []
|
|
if (typeof value === "string") return [value]
|
|
if (Array.isArray(value)) return value
|
|
if (value?.type === "FeatureCollection" && Array.isArray(value.features)) return value.features
|
|
if (value?.type === "Feature" || isGeoJsonGeometry(value)) return [value]
|
|
if (Array.isArray(value?.rows)) return value.rows
|
|
if (Array.isArray(value?.data)) return value.data
|
|
if (value?.data) return normalizePastureGrasslandRows(value.data)
|
|
if (Array.isArray(value?.list)) return value.list
|
|
if (Array.isArray(value?.features)) return value.features
|
|
if (value?.wkt || value?.geo || value?.geom || value?.geometryWkt || value?.geometry_wkt || value?.shape || value?.location || value?.geometry) return [value]
|
|
return []
|
|
}
|
|
|
|
function normalizePastureGrasslandGeometry(row) {
|
|
if (typeof row === "string") return parseGeometryText(row)
|
|
if (row?.type === "Feature") return normalizePastureGrasslandGeometry(row.geometry)
|
|
if (isGeoJsonGeometry(row)) return row
|
|
if (typeof row?.geometry === "string") return parseGeometryText(row.geometry)
|
|
if (row?.geometry?.type === "Feature") return normalizePastureGrasslandGeometry(row.geometry.geometry)
|
|
if (isGeoJsonGeometry(row?.geometry)) return row.geometry
|
|
return parseGeometryText(row?.wkt || row?.geo || row?.geom || row?.geometryWkt || row?.geometry_wkt || row?.shape || row?.location)
|
|
}
|
|
|
|
function normalizePastureGrasslandProperties(row) {
|
|
if (!row || typeof row !== "object" || Array.isArray(row)) return { wkt: row }
|
|
if (row.type === "Feature") return row.properties || {}
|
|
return row
|
|
}
|
|
|
|
function parseGeometryText(value) {
|
|
const text = String(value || "").trim()
|
|
if (!text) return null
|
|
if (text.startsWith("{") || text.startsWith("[")) {
|
|
try {
|
|
const parsed = JSON.parse(text)
|
|
if (parsed?.type === "Feature") return normalizePastureGrasslandGeometry(parsed)
|
|
if (isGeoJsonGeometry(parsed)) return parsed
|
|
} catch (error) {
|
|
return null
|
|
}
|
|
}
|
|
return parseWktGeometry(text)
|
|
}
|
|
|
|
function isGeoJsonGeometry(value) {
|
|
return Boolean(
|
|
value &&
|
|
["Polygon", "MultiPolygon", "LineString", "MultiLineString", "Point", "MultiPoint"].includes(value.type) &&
|
|
value.coordinates
|
|
)
|
|
}
|
|
|
|
function parseWktGeometry(wkt) {
|
|
const text = String(wkt || "").trim()
|
|
if (!text) return null
|
|
const typeMatch = text.match(/^([A-Z]+)(?:\s+[A-Z]+)?\s*\(/i)
|
|
const type = typeMatch?.[1]?.toUpperCase()
|
|
if (!type) return null
|
|
const body = text.slice(text.indexOf("("))
|
|
if (type === "POLYGON") return { type: "Polygon", coordinates: parseWktPolygonBody(body) }
|
|
if (type === "MULTIPOLYGON") return { type: "MultiPolygon", coordinates: getWktChildGroups(body).map(parseWktPolygonBody).filter((polygon) => polygon.length) }
|
|
if (type === "POINT") return { type: "Point", coordinates: parseWktPoint(body) }
|
|
if (type === "MULTIPOINT") return { type: "MultiPoint", coordinates: getWktChildGroups(body).map(parseWktPoint).filter(Boolean) }
|
|
if (type === "LINESTRING") return { type: "LineString", coordinates: parseWktLine(body) }
|
|
if (type === "MULTILINESTRING") return { type: "MultiLineString", coordinates: getWktChildGroups(body).map(parseWktLine).filter((line) => line.length) }
|
|
return null
|
|
}
|
|
|
|
function parseWktPolygonBody(body) {
|
|
return getWktChildGroups(body)
|
|
.map(parseWktLine)
|
|
.filter((ring) => ring.length >= 3)
|
|
}
|
|
|
|
function parseWktLine(text) {
|
|
return String(text || "").replace(/^\(+|\)+$/g, "").split(",").map(parseWktPoint).filter(Boolean)
|
|
}
|
|
|
|
function parseWktPoint(text) {
|
|
const point = String(text || "").replace(/^\(+|\)+$/g, "").trim().split(/\s+/).slice(0, 2).map(Number)
|
|
return point.length >= 2 && Number.isFinite(point[0]) && Number.isFinite(point[1]) ? [point[0], point[1]] : null
|
|
}
|
|
|
|
function getWktChildGroups(text) {
|
|
const trimmed = String(text || "").trim()
|
|
const inner = trimmed.startsWith("(") && trimmed.endsWith(")") ? trimmed.slice(1, -1) : trimmed
|
|
const groups = []
|
|
let depth = 0
|
|
let start = -1
|
|
Array.from(inner).forEach((char, index) => {
|
|
if (char === "(") {
|
|
if (depth === 0) start = index
|
|
depth += 1
|
|
} else if (char === ")") {
|
|
depth -= 1
|
|
if (depth === 0 && start >= 0) {
|
|
groups.push(inner.slice(start, index + 1))
|
|
start = -1
|
|
}
|
|
}
|
|
})
|
|
return groups.length ? groups : [inner]
|
|
}
|
|
|
|
function createPastureFocusFeatures(payload = {}) {
|
|
const format = new GeoJSON()
|
|
return [
|
|
...normalizePastureGrasslandRows(payload?.grasslands),
|
|
...normalizePastureGrasslandRows(payload?.wkt),
|
|
]
|
|
.map((row, index) => {
|
|
const geometry = normalizePastureGrasslandGeometry(row)
|
|
if (!geometry) return null
|
|
const properties = normalizePastureGrasslandProperties(row)
|
|
const feature = format.readFeature(
|
|
{
|
|
type: "Feature",
|
|
properties: {
|
|
...properties,
|
|
id: properties.id || properties.gid || `${payload?.name || "pasture"}-${index}`,
|
|
pasture_name: payload?.name || properties.pastureName || properties.name,
|
|
},
|
|
geometry,
|
|
},
|
|
{
|
|
dataProjection: "EPSG:4326",
|
|
featureProjection: "EPSG:3857",
|
|
},
|
|
)
|
|
feature.setId(properties.id || properties.gid || `${payload?.name || "pasture"}-${index}`)
|
|
return feature
|
|
})
|
|
.filter(Boolean)
|
|
}
|
|
|
|
function clearPastureFocusYaks() {
|
|
pastureFocusYakRequestId += 1
|
|
pastureFocusYakSource?.clear(true)
|
|
}
|
|
|
|
function createPastureFocusYakFeatures(rows = []) {
|
|
const format = new GeoJSON()
|
|
return normalizePastureGrasslandRows(rows)
|
|
.map((row, index) => {
|
|
const geometry = normalizePastureGrasslandGeometry(row)
|
|
if (!geometry) return null
|
|
const properties = normalizePastureGrasslandProperties(row)
|
|
const feature = format.readFeature(
|
|
{
|
|
type: "Feature",
|
|
properties: {
|
|
...properties,
|
|
id: properties.id || properties.yak_id || properties.yakId || `focus-yak-${index}`,
|
|
},
|
|
geometry,
|
|
},
|
|
{
|
|
dataProjection: "EPSG:4326",
|
|
featureProjection: "EPSG:3857",
|
|
},
|
|
)
|
|
feature.setId(properties.id || properties.yak_id || properties.yakId || `focus-yak-${index}`)
|
|
return feature
|
|
})
|
|
.filter(Boolean)
|
|
}
|
|
|
|
function loadPastureFocusYaks(rows = []) {
|
|
if (!olMap) return 0
|
|
if (!pastureFocusYakSource) {
|
|
olMap.addLayer(createPastureFocusYakLayer())
|
|
}
|
|
const requestId = ++pastureFocusYakRequestId
|
|
const yakFeatures = createPastureFocusYakFeatures(rows)
|
|
if (requestId !== pastureFocusYakRequestId) return 0
|
|
pastureFocusYakSource.clear(true)
|
|
pastureFocusYakSource.addFeatures(yakFeatures)
|
|
emitLayerStatus(
|
|
yakFeatures.length ? "success" : "warning",
|
|
yakFeatures.length ? `已加载选中草场牦牛识别${yakFeatures.length}处` : "选中草场暂无牦牛识别标记",
|
|
)
|
|
return yakFeatures.length
|
|
}
|
|
|
|
function focusPasture(payload = {}) {
|
|
if (!olMap) return false
|
|
clearFloatingOverlays()
|
|
const features = createPastureFocusFeatures(payload)
|
|
if (!features.length) {
|
|
emitLayerStatus("warning", "当前牧户暂无可定位数据")
|
|
return false
|
|
}
|
|
if (!pastureFocusSource) {
|
|
olMap.addLayer(createPastureFocusLayer())
|
|
}
|
|
pastureFocusSource.clear(true)
|
|
pastureFocusSource.addFeatures(features)
|
|
const extent = pastureFocusSource.getExtent()
|
|
if (!extent || extent.some((value) => !Number.isFinite(value))) {
|
|
emitLayerStatus("warning", "当前牧户暂无可定位数据")
|
|
return false
|
|
}
|
|
const size = olMap.getSize?.() || [1280, 720]
|
|
const horizontalPadding = Math.min(360, Math.max(96, Number(size[0] || 0) * 0.24))
|
|
olMap.getView()?.fit(extent, {
|
|
duration: 700,
|
|
maxZoom: 17,
|
|
padding: [96, horizontalPadding, 96, horizontalPadding],
|
|
})
|
|
loadPastureFocusYaks(payload?.yakMarks || payload?.yakMarksGeoJson || payload?.yaks || [])
|
|
emitLayerStatus("success", "已定位具体草场")
|
|
window.setTimeout(() => olMap?.updateSize?.(), 0)
|
|
return true
|
|
}
|
|
|
|
function clearPastureFocus() {
|
|
clearFloatingOverlays()
|
|
pastureFocusSource?.clear(true)
|
|
clearPastureFocusYaks()
|
|
}
|
|
|
|
function createTownLabelStyle(feature, resolution) {
|
|
const name = feature.get("name") || ""
|
|
const viewResolution = Number(resolution) || 0
|
|
const compact = viewResolution > 180
|
|
const close = viewResolution < 55
|
|
const hidden = viewResolution > 520
|
|
if (hidden) return labelPointStyle
|
|
const fontSize = close ? 14 : compact ? 11 : 12.5
|
|
const minWidthPadding = close ? [6, 12, 6, 12] : compact ? [4, 8, 4, 8] : [5, 10, 5, 10]
|
|
const triangleRadius = close ? 5 : compact ? 3.8 : 4.4
|
|
const offsetY = close ? -20 : compact ? -15 : -17
|
|
return [
|
|
new Style({
|
|
image: new RegularShape({
|
|
points: 3,
|
|
radius: triangleRadius,
|
|
rotation: Math.PI,
|
|
displacement: [0, close ? 16 : compact ? 12 : 14],
|
|
fill: new Fill({ color: "rgba(126, 251, 246, 0.98)" }),
|
|
stroke: new Stroke({ color: "rgba(4, 28, 40, 0.95)", width: 1.2 }),
|
|
}),
|
|
}),
|
|
new Style({
|
|
text: new Text({
|
|
text: name,
|
|
font: `800 ${fontSize}px "Microsoft YaHei", "PingFang SC", sans-serif`,
|
|
fill: new Fill({ color: "#ffffff" }),
|
|
stroke: new Stroke({ color: "rgba(2, 18, 28, 1)", width: 5 }),
|
|
backgroundFill: new Fill({ color: "rgba(3, 21, 33, 0.9)" }),
|
|
backgroundStroke: new Stroke({ color: "rgba(126, 251, 246, 0.58)", width: 1.2 }),
|
|
padding: minWidthPadding,
|
|
offsetY,
|
|
}),
|
|
}),
|
|
]
|
|
}
|
|
|
|
function formatCoordinate(value) {
|
|
const number = Number(value)
|
|
if (!Number.isFinite(number)) return "0.000000"
|
|
return number.toFixed(6)
|
|
}
|
|
|
|
async function copyTextToClipboard(text) {
|
|
if (typeof navigator !== "undefined" && navigator.clipboard?.writeText) {
|
|
try {
|
|
await navigator.clipboard.writeText(text)
|
|
return
|
|
} catch (error) {
|
|
// Fall back to textarea copy below. Some browsers do not allow clipboard writes from contextmenu.
|
|
}
|
|
}
|
|
const textarea = document.createElement("textarea")
|
|
textarea.value = text
|
|
textarea.setAttribute("readonly", "readonly")
|
|
textarea.style.position = "fixed"
|
|
textarea.style.left = "-9999px"
|
|
textarea.style.top = "0"
|
|
textarea.style.opacity = "0"
|
|
try {
|
|
document.body.appendChild(textarea)
|
|
textarea.focus({ preventScroll: true })
|
|
textarea.select()
|
|
textarea.setSelectionRange(0, text.length)
|
|
const success = document.execCommand("copy")
|
|
if (!success) throw new Error("copy failed")
|
|
} finally {
|
|
document.body.removeChild(textarea)
|
|
}
|
|
}
|
|
|
|
function showCoordinateCopyTip(event, text, error = false) {
|
|
if (coordinateTipTimer) window.clearTimeout(coordinateTipTimer)
|
|
coordinateCopyMenu.value = {
|
|
...coordinateCopyMenu.value,
|
|
visible: false,
|
|
}
|
|
const tipWidth = 270
|
|
const tipHeight = 32
|
|
const viewportWidth = window.innerWidth || 1920
|
|
const viewportHeight = window.innerHeight || 1080
|
|
coordinateCopyTip.value = {
|
|
visible: true,
|
|
text,
|
|
x: Math.min(Math.max(12, event.clientX + 12), Math.max(12, viewportWidth - tipWidth)),
|
|
y: Math.min(Math.max(12, event.clientY + 12), Math.max(12, viewportHeight - tipHeight)),
|
|
error,
|
|
}
|
|
coordinateTipTimer = window.setTimeout(() => {
|
|
coordinateCopyTip.value = {
|
|
...coordinateCopyTip.value,
|
|
visible: false,
|
|
}
|
|
coordinateTipTimer = null
|
|
}, 1500)
|
|
}
|
|
|
|
function showCoordinateCopyMenu(event, text, error = false) {
|
|
if (coordinateTipTimer) {
|
|
window.clearTimeout(coordinateTipTimer)
|
|
coordinateTipTimer = null
|
|
}
|
|
const menuWidth = 300
|
|
const menuHeight = 38
|
|
const viewportWidth = window.innerWidth || 1920
|
|
const viewportHeight = window.innerHeight || 1080
|
|
coordinateCopyTip.value = {
|
|
...coordinateCopyTip.value,
|
|
visible: false,
|
|
}
|
|
coordinateCopyMenu.value = {
|
|
visible: true,
|
|
text,
|
|
x: Math.min(Math.max(12, event.clientX + 12), Math.max(12, viewportWidth - menuWidth)),
|
|
y: Math.min(Math.max(12, event.clientY + 12), Math.max(12, viewportHeight - menuHeight)),
|
|
error,
|
|
}
|
|
}
|
|
|
|
function hideCoordinateCopyMenu() {
|
|
coordinateCopyMenu.value = {
|
|
...coordinateCopyMenu.value,
|
|
visible: false,
|
|
}
|
|
}
|
|
|
|
function clearFloatingOverlays() {
|
|
if (coordinateTipTimer) {
|
|
window.clearTimeout(coordinateTipTimer)
|
|
coordinateTipTimer = null
|
|
}
|
|
coordinateCopyTip.value = {
|
|
...coordinateCopyTip.value,
|
|
visible: false,
|
|
}
|
|
hideCoordinateCopyMenu()
|
|
}
|
|
|
|
async function copyCoordinateFromMapEvent(event) {
|
|
if (!olMap) return
|
|
const pixel = olMap.getEventPixel(event)
|
|
const coordinate = olMap.getCoordinateFromPixel(pixel)
|
|
if (!coordinate) return
|
|
const [lng, lat] = toLonLat(coordinate)
|
|
const text = `${formatCoordinate(lng)}, ${formatCoordinate(lat)}`
|
|
try {
|
|
await copyTextToClipboard(text)
|
|
showCoordinateCopyTip(event, `已复制 ${text}`)
|
|
} catch (error) {
|
|
showCoordinateCopyMenu(event, text, true)
|
|
}
|
|
}
|
|
|
|
function handleRightPointerDown(event) {
|
|
if (event.button !== 2) {
|
|
hideCoordinateCopyMenu()
|
|
return
|
|
}
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
lastRightClickCopyTime = Date.now()
|
|
copyCoordinateFromMapEvent(event)
|
|
}
|
|
|
|
async function handleCoordinateMenuCopy() {
|
|
const text = coordinateCopyMenu.value.text
|
|
if (!text) return
|
|
try {
|
|
await copyTextToClipboard(text)
|
|
coordinateCopyMenu.value = {
|
|
...coordinateCopyMenu.value,
|
|
visible: false,
|
|
error: false,
|
|
}
|
|
coordinateCopyTip.value = {
|
|
visible: true,
|
|
text: `已复制 ${text}`,
|
|
x: coordinateCopyMenu.value.x,
|
|
y: coordinateCopyMenu.value.y,
|
|
error: false,
|
|
}
|
|
if (coordinateTipTimer) window.clearTimeout(coordinateTipTimer)
|
|
coordinateTipTimer = window.setTimeout(() => {
|
|
coordinateCopyTip.value = {
|
|
...coordinateCopyTip.value,
|
|
visible: false,
|
|
}
|
|
coordinateTipTimer = null
|
|
}, 1500)
|
|
} catch (error) {
|
|
coordinateCopyMenu.value = {
|
|
...coordinateCopyMenu.value,
|
|
error: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
function handleContextMenu(event) {
|
|
event.preventDefault()
|
|
event.stopPropagation()
|
|
if (Date.now() - lastRightClickCopyTime < 800) return
|
|
lastRightClickCopyTime = Date.now()
|
|
copyCoordinateFromMapEvent(event)
|
|
}
|
|
|
|
function handleMapSingleClick(event) {
|
|
if (!olMap || !event?.coordinate) return
|
|
hideCoordinateCopyMenu()
|
|
const [lng, lat] = toLonLat(event.coordinate)
|
|
if (!Number.isFinite(lng) || !Number.isFinite(lat)) return
|
|
const sourceEvent = event.originalEvent || {}
|
|
emit("map-click", {
|
|
lngLat: { lng, lat },
|
|
coordinate: event.coordinate,
|
|
pixel: event.pixel,
|
|
screen: {
|
|
x: Number(sourceEvent.clientX),
|
|
y: Number(sourceEvent.clientY),
|
|
},
|
|
overlayLayer: props.overlayLayer,
|
|
overlayLayers: normalizeOverlayLayers(),
|
|
})
|
|
}
|
|
|
|
function bindContextMenuCopy() {
|
|
const viewport = olMap?.getViewport?.()
|
|
if (!viewport || contextMenuHandler) return
|
|
rightPointerDownHandler = (event) => {
|
|
handleRightPointerDown(event)
|
|
}
|
|
contextMenuHandler = (event) => {
|
|
handleContextMenu(event)
|
|
}
|
|
viewport.addEventListener("pointerdown", rightPointerDownHandler)
|
|
viewport.addEventListener("contextmenu", contextMenuHandler)
|
|
}
|
|
|
|
function unbindContextMenuCopy() {
|
|
const viewport = olMap?.getViewport?.()
|
|
if (viewport && rightPointerDownHandler) {
|
|
viewport.removeEventListener("pointerdown", rightPointerDownHandler)
|
|
}
|
|
if (viewport && contextMenuHandler) {
|
|
viewport.removeEventListener("contextmenu", contextMenuHandler)
|
|
}
|
|
rightPointerDownHandler = null
|
|
contextMenuHandler = null
|
|
clearFloatingOverlays()
|
|
}
|
|
|
|
async function initMap() {
|
|
if (olMap) return
|
|
await nextTick()
|
|
if (!mapEl.value || olMap) return
|
|
|
|
const initialZoom = normalizeInitialZoom()
|
|
resetTileState(initialZoom)
|
|
const view = new View({
|
|
center: fromLonLat(normalizeCenter()),
|
|
zoom: initialZoom,
|
|
minZoom: props.minZoom,
|
|
maxZoom: props.maxZoom,
|
|
enableRotation: false,
|
|
constrainResolution: false,
|
|
smoothResolutionConstraint: true,
|
|
})
|
|
|
|
olMap = new Map({
|
|
target: mapEl.value,
|
|
controls: defaultControls({
|
|
attribution: false,
|
|
rotate: false,
|
|
zoom: false,
|
|
}),
|
|
layers: [
|
|
new TileLayer({
|
|
source: buildBaseSource(),
|
|
preload: 1,
|
|
zIndex: 1,
|
|
}),
|
|
...createBoundaryLayers(),
|
|
createPastureFocusLayer(),
|
|
createPastureFocusYakLayer(),
|
|
],
|
|
view,
|
|
})
|
|
refreshOverlayLayer()
|
|
bindContextMenuCopy()
|
|
|
|
eventKeys.push(
|
|
olMap.on("singleclick", handleMapSingleClick),
|
|
)
|
|
|
|
eventKeys.push(
|
|
view.on("change:resolution", () => {
|
|
const zoom = currentZoom()
|
|
if (zoom !== tileState.zoom) {
|
|
resetTileState(zoom)
|
|
emitTileStatus("loading", `正在加载 hy-result ${zoom}级影像底图`)
|
|
}
|
|
}),
|
|
)
|
|
|
|
window.setTimeout(() => {
|
|
olMap?.updateSize?.()
|
|
emit("ready")
|
|
}, 0)
|
|
}
|
|
|
|
function disposeMap() {
|
|
unbindContextMenuCopy()
|
|
if (eventKeys.length) {
|
|
unByKey(eventKeys)
|
|
eventKeys = []
|
|
}
|
|
if (wmsEventKeys.length) {
|
|
unByKey(wmsEventKeys)
|
|
wmsEventKeys = []
|
|
}
|
|
pendingTiles.clear()
|
|
baseSource = null
|
|
overlaySource = null
|
|
overlaySources = []
|
|
overlayMapLayer = null
|
|
overlayMapLayers = []
|
|
pastureFocusSource = null
|
|
pastureFocusLayer = null
|
|
pastureFocusYakSource = null
|
|
pastureFocusYakLayer = null
|
|
pastureFocusYakRequestId += 1
|
|
if (olMap) {
|
|
olMap.setTarget(null)
|
|
olMap.dispose?.()
|
|
olMap = null
|
|
}
|
|
}
|
|
|
|
function refreshView() {
|
|
clearFloatingOverlays()
|
|
pastureFocusSource?.clear(true)
|
|
clearPastureFocusYaks()
|
|
locateMap()
|
|
olMap?.updateSize?.()
|
|
window.setTimeout(() => olMap?.updateSize?.(), 120)
|
|
}
|
|
|
|
function refreshTileSources() {
|
|
if (baseSource) {
|
|
resetTileState(currentZoom())
|
|
baseSource.refresh?.()
|
|
emitTileStatus("loading", `正在刷新 hy-result ${tileState.zoom}级影像底图`)
|
|
}
|
|
const sources = overlaySources.length ? overlaySources : [overlaySource].filter(Boolean)
|
|
sources.forEach((source) => source?.refresh?.())
|
|
}
|
|
|
|
function resetView() {
|
|
clearFloatingOverlays()
|
|
pastureFocusSource?.clear(true)
|
|
clearPastureFocusYaks()
|
|
locateMap()
|
|
refreshTileSources()
|
|
olMap?.updateSize?.()
|
|
window.setTimeout(() => olMap?.updateSize?.(), 80)
|
|
window.setTimeout(() => olMap?.updateSize?.(), 220)
|
|
}
|
|
|
|
function hasMap() {
|
|
return Boolean(olMap)
|
|
}
|
|
|
|
defineExpose({
|
|
refreshView,
|
|
resetView,
|
|
hasMap,
|
|
focusPasture,
|
|
clearPastureFocus,
|
|
clearFloatingOverlays,
|
|
})
|
|
|
|
onMounted(() => {
|
|
initMap()
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
disposeMap()
|
|
})
|
|
|
|
watch(
|
|
() => props.active,
|
|
(active) => {
|
|
if (active) {
|
|
initMap()
|
|
window.setTimeout(() => {
|
|
locateMap()
|
|
olMap?.updateSize?.()
|
|
}, 0)
|
|
}
|
|
},
|
|
)
|
|
|
|
watch(
|
|
() => props.center,
|
|
() => {
|
|
locateMap()
|
|
},
|
|
{ deep: true },
|
|
)
|
|
|
|
watch(
|
|
() => props.tileUrlTemplate,
|
|
() => {
|
|
if (!baseSource) return
|
|
resetTileState(currentZoom())
|
|
baseSource.setUrl(props.tileUrlTemplate)
|
|
baseSource.refresh()
|
|
emitTileStatus("loading", `正在加载 hy-result ${tileState.zoom}级影像底图`)
|
|
},
|
|
)
|
|
|
|
watch(
|
|
() => [props.overlayLayer, props.overlayLayers],
|
|
() => {
|
|
refreshOverlayLayer()
|
|
},
|
|
{ deep: true },
|
|
)
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.yak-ol-map {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 1;
|
|
overflow: hidden;
|
|
background: #03121c;
|
|
}
|
|
|
|
.yak-ol-map__canvas {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
.yak-ol-map__copy-tip {
|
|
position: fixed;
|
|
z-index: 30;
|
|
max-width: 260px;
|
|
padding: 7px 10px;
|
|
border: 1px solid rgba(126, 251, 246, 0.4);
|
|
border-radius: 3px;
|
|
color: rgba(235, 255, 255, 0.94);
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
background: rgba(3, 21, 33, 0.86);
|
|
box-shadow: 0 0 14px rgba(48, 220, 255, 0.16);
|
|
pointer-events: none;
|
|
transform: translateY(-50%);
|
|
white-space: nowrap;
|
|
|
|
&.is-error {
|
|
border-color: rgba(255, 143, 107, 0.55);
|
|
color: #ffe0d6;
|
|
}
|
|
}
|
|
|
|
.yak-ol-map__copy-menu {
|
|
position: fixed;
|
|
z-index: 30;
|
|
display: flex;
|
|
height: 34px;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 0 8px 0 10px;
|
|
border: 1px solid rgba(126, 251, 246, 0.42);
|
|
border-radius: 3px;
|
|
color: rgba(235, 255, 255, 0.94);
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
background: rgba(3, 21, 33, 0.92);
|
|
box-shadow: 0 0 14px rgba(48, 220, 255, 0.18);
|
|
pointer-events: all;
|
|
white-space: nowrap;
|
|
|
|
span {
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
button {
|
|
height: 22px;
|
|
padding: 0 8px;
|
|
border: 1px solid rgba(126, 251, 246, 0.38);
|
|
border-radius: 2px;
|
|
color: #031822;
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
background: linear-gradient(180deg, #7efbf6, #30dcff);
|
|
cursor: pointer;
|
|
}
|
|
|
|
&.is-error {
|
|
border-color: rgba(255, 215, 108, 0.48);
|
|
}
|
|
}
|
|
|
|
:deep(.ol-viewport) {
|
|
background: #03121c;
|
|
}
|
|
</style>
|
|
|