先看效果
主要功能如下:
- 测量
- 图源更换
- 放大缩小
- 地图添加点
- hover点数据
- 切换到地图位置;也设定层级
- 2D3D切换,3D为cesium开发,
- 技术交流可以加V:bloxed
地图工具做了插槽,分为toolbar(左上角工具) 和mode(右下角功能区),对应mapTools.vue和mapMode.vue 文件
上代码:
commonmap.vue文件
<!--
* @Author: shangyc
* @Date: 2024-011-07 09:54:54
* @LastEditors: shangyc
* @LastEditTime: 2024-011-07 09:54:54
* @Description: file content
-->
<template>
<div class="map-container h100 w100">
<div class="mouseInfo" v-show="mouseInfo.scale">
<span class="btn-item">坐标:{{ mouseInfo.coords }}</span>
<span class="btn-item">比例尺:{{ mouseInfo.scale }}</span>
<span class="btn-item">视野:{{ mouseInfo.zoom }}</span>
</div>
<div id="map" class="map h100"></div>
<slot name="toolbar" ></slot>
<slot name="mode"></slot>
<!-- tooltip -->
<div ref="tooltip" class="tooltip" v-if="tooltipVisible">
<el-row v-if="tooltipContents.length">
<el-col v-for="item in tooltipContents" :key="item.value" @click="tooltipClick" class="content-col">
<span>{{ item.name }}:</span>
<span>{{ item.value }}</span>
<span> {{ item.unit ?? "" }}</span>
</el-col>
</el-row>
<el-row v-else>
<el-col class="content-col">暂无数据</el-col>
</el-row>
</div>
<!--end -->
</div>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref } from "vue";
import Map from "ol/Map";
import TileLayer from "ol/layer/Tile";
import View from "ol/View";
import { defaults as defaultControls } from "ol/control";
import { Overlay } from "ol";
import { fromLonLat, get as getProjection, toLonLat } from "ol/proj";
import { Cluster, Vector as SourceVector, XYZ } from "ol/source";
import { Vector as LayerVector } from "ol/layer";
import GeoJSON from "ol/format/GeoJSON";
import { Fill, Icon, Stroke, Style, Text } from "ol/style";
import CircleStyle from "ol/style/Circle";
import { Polygon } from "ol/geom";
import LineString from "ol/geom/LineString";
import { getArea, getLength } from "ol/sphere";
import GeometryType from "ol/geom/GeometryType";
import { Draw } from "ol/interaction";
import { unByKey } from "ol/Observable";
import OverlayPositioning from "ol/OverlayPositioning";
import areaLabel from "/@/assets/map/areaLabel.png";
import Mask from "ol-ext/filter/Mask";
import Crop from "ol-ext/filter/Crop";
import { getVectorContext } from "ol/render";
const { VITE_APP_2DMAP_URL } = import.meta.env;
const tooltipVisible = ref<boolean>(false);
const props = defineProps({
imageSource: {
type: String,
default: "img",
},
toolBox: {
type: Boolean,
default: true,
},
});
const imgSource = ref<string>("yx");
const emit = defineEmits(["mapSingleClick"]); // 自定义事件 会回传也是point数据
const center:any = [];
// start地图以及图层显示
const map = ref<any>(null);
//鼠标信息
const mouseInfo = reactive<any>({
coords: "",
scale: "",
zoom: "",
});
let vector: any;
const drawObj = ref<any>(null);
const helpTooltipElement = ref<any>(null);
const helpTooltip = ref<any>(null);
const measureTooltipElement = ref<any>(null);
const measureTooltip = ref<any>(null);
let timer = null;
let flashTimer = null;
const clickPoint = ref<any>();
const tooltip = ref(null);
const tooltipContents = ref<any[]>([]);
//底图图源
const env = import.meta.env.VITE_APP_ENV;
const imgpublicUrl = `${VITE_APP_2DMAP_URL}/yx/{z}/{x}/{-y}.png`;
const cvapublicUrl = `${VITE_APP_2DMAP_URL}/dz/{z}/{x}/{-y}.png`;
const imgLayer = ref<any>(
new TileLayer({
source: new XYZ({
// wrapX: true,
url: imgpublicUrl
}),
visible: true
})
);
const vecLayer = ref<any>(
new TileLayer({
source: new XYZ({
url: cvapublicUrl,
projection: getProjection("EPSG:3857"),
// wrapX: true
}),
visible: true
})
);
// 用于存储事件监听器键的数组
const eventKeys = ref<any[]>([]);
let start = new Date().getTime();
const disableMapEvent = ref<boolean>(false); // 关闭地图上面事件
//地图操作事件
const zoomInOut = (number: any) => {
if (map.value) {
const view = map.value.getView();
const zoom = view.getZoom();
view.setZoom(zoom + number);
}
};
const toCenter = (coordinates?: any[]) => {
if (map.value) {
map.value.getView().animate({
center: fromLonLat(coordinates ?? center),
duration: 2500, // 动画持续时间,单位为毫秒(这里设置为2秒)
});
}
};
const points: any = {
type: "FeatureCollection",
features: [],
};
//查询到站点添加到地图
const addStation = (stations: any[]) => {
mapInteractionSource.value.clear();
map.value?.getOverlays().clear();
points.features = [
];
stations.forEach((point, index) => {
point.lng &&
point.lat &&
points.features.push({
type: "Feature",
geometry: {
type: "Point",
coordinates: fromLonLat([point.lng, point.lat]),
},
properties: {
id: index + point.stcd,
name: point.stnm,
type: point.sttp,
info: point,
icon: point.icon,
},
id: index + point.stcd,
});
});
new GeoJSON({ featureProjection: "EPSG:4326" })
.readFeatures({
type: "FeatureCollection",
features: [...points.features],
})
.forEach((item: any) => {
mapInteractionSource.value.addFeature(item);
});
};
const mapInteractionSource = ref<any>(new SourceVector({ wrapX: false }));
const imagSourceClick = (type: any) => {
imgSource.value = type;
if (type == "yx") {
imgLayer.value.setVisible(true);
vecLayer.value.setVisible(false);
} else {
imgLayer.value.setVisible(false);
vecLayer.value.setVisible(true);
}
};
const initMap = () => {
if (map.value) {
map.value.setTarget(null);
}
// 聚合
var cluster = new Cluster({
source: mapInteractionSource.value,
distance: 30,
});
const cluster1Style = (feature: any, resolution: any) => {
const { name, icon } = feature.get("features")[0].getProperties();
return new Style({
//把点的样式换成ICON图标
fill: new Fill({
//填充颜色
color: "rgba(37,241,239,0.2)",
}),
//图形样式,主要适用于点样式
image: new Icon({
opacity: 1,
scale: 0.5,
src: icon ?? areaLabel,
}),
text: new Text({
// 字体与大小
font: "bold 13px Microsoft YaHei",
//文字填充色
fill: new Fill({
color: "#fff",
}),
// 显示文本,数字需要转换为文本string类型!
text: name,
offsetY: -35,
}),
});
};
map.value = new Map({
layers: [
vecLayer.value,
imgLayer.value,
new LayerVector({
source: cluster,
style: cluster1Style,
}),
],
keyboardEventTarget: document,
target: "map", // 对应页面里 id 为 map 的元素
view: new View({
center: fromLonLat([110.105931,22.422299]),
zoom: 15,
}),
//控件初始默认不显示
controls: defaultControls({
attribution: false,
zoom: false,
rotate: false,
}).extend([]),
});
const tdtStyle = new Style({
fill: new Fill({
color: "black",
}),
});
imgLayer.value.on("postrender", (e: any) => {
const vectorContext = getVectorContext(e);
e.context.globalCompositeOperation = "destination-in";
e.context.globalCompositeOperation = "source-over";
});
map.value.on("pointermove", handleMouseMove);
map.value.on("pointermove", (evt: any) => {
const pixel = map?.value?.getEventPixel(evt.originalEvent);
const hit = map?.value?.hasFeatureAtPixel(pixel);
map.value.forEachFeatureAtPixel(evt.pixel, function (feature: any) {
let geometry: any = null;
try {
geometry = JSON.parse(new GeoJSON().writeFeature(feature));
} catch (error) {
geometry = feature;
}
if (geometry.geometry.type == "Point") {
const coordinate = geometry.geometry.coordinates;
const data =
geometry.properties?.features[0]?.values_?.info?.data || [];
if (data.length) {
clickPoint.value = geometry.properties?.features[0]?.values_?.info;
tooltipContents.value = data;
tooltipVisible.value = true;
try {
map.value.addOverlay(
new Overlay({
position: coordinate,
offset: [0, 0],
element: tooltip.value,
stopEvent: true,
})
);
} catch (error) {
console.warn(error);
}
}
}
});
});
map.value.on("singleclick", (evt: any) => {
var pixel = map?.value?.getEventPixel(evt.originalEvent);
var hit = map?.value?.hasFeatureAtPixel(pixel);
map.value.forEachFeatureAtPixel(evt.pixel, function (feature: any) {
let geometry: any = null;
try {
geometry = JSON.parse(new GeoJSON().writeFeature(feature));
} catch (error) {
geometry = feature;
}
if (geometry.geometry.type == "Point") {
const coordinate = geometry.geometry.coordinates;
mapSingleClick(geometry.properties?.features[0]?.values_?.info);
const data =
geometry.properties?.features[0]?.values_?.info?.data || [];
if (data.length) {
clickPoint.value = geometry.properties?.features[0]?.values_?.info;
tooltipClick();
}
}
});
});
};
const mapSingleClick = (point: any) => {
emit("mapSingleClick", point);
};
const handleMouseMove = (event: any) => {
const coordinate = event.coordinate;
const lonLat = toLonLat(map.value?.getCoordinateFromPixel(event.pixel));
const scale = map.value?.getView().getResolution(); // 近似计算比例尺(米)
const zoom = map.value?.getView().getZoom();
mouseInfo.coords = ` ${lonLat[0].toFixed(5)}, ${lonLat[1].toFixed(5)}`;
mouseInfo.scale = `1:${ Math.round(scale * 10000) / 100} m`;
mouseInfo.zoom = `${zoom.toFixed(0)} 级`;
};
/**
* 绘画
* @param measureType line
*/
const draw = (measureType: any) => {
// 移除所有事件监听器
// eventKeys.value.forEach(key => unByKey(key));
// eventKeys.value = []; // 清空数组,以便后续可能重新添加事件
if (disableMapEvent.value) {
return ElMessage.warning("请先结束之前的绘制");
}
drawObj.value && map.value.removeInteraction(drawObj.value);
const source = new SourceVector();
// if (!vector) {
vector = new LayerVector({
name: "drawLayer",
zIndex: 10000,
source: source,
style: new Style({
fill: new Fill({
color: "rgba(255, 255, 255, 0.2)",
}),
stroke: new Stroke({
color: "#ffcc33",
width: 2,
}),
image: new CircleStyle({
radius: 7,
fill: new Fill({
color: "#ffcc33",
}),
}),
}),
});
map.value.addLayer(vector);
// }
/**
* Currently drawn feature.
* @type {module:ol/Feature~Feature}
*/
let sketch: any = null;
/**
* Message to show when the user is drawing a polygon.
* @type {string}
*/
const continuePolygonMsg = "继续点击绘制多边形";
/**
* Message to show when the user is drawing a line.
* @type {string}
*/
const continueLineMsg = "继续点击绘制线";
/**
* Handle pointer move.
* @param {module:ol/MapBrowserEvent~MapBrowserEvent} evt The event.
*/
const pointerMoveHandler = (evt: any) => {
if (evt.dragging) {
return;
}
/** @type {string} */
let helpMsg = "请点击开始绘制";
if (sketch) {
const geom = sketch.getGeometry();
if (geom instanceof Polygon) {
helpMsg = continuePolygonMsg;
} else if (geom instanceof LineString) {
helpMsg = continueLineMsg;
}
}
helpTooltipElement.value.innerHTML = `<div style="color:#fff;background-color: rgba(0,0,0,0.6);padding: 4px;border-radius: 6px">${helpMsg}</div>`;
helpTooltip.value.setPosition(evt.coordinate);
helpTooltipElement.value?.lassList?.remove("hidden");
};
map.value.on("pointermove", pointerMoveHandler);
map.value.getViewport().addEventListener("mouseout", function () {
helpTooltipElement.value?.classList?.add("hidden");
});
const formatLength = (line: any) => {
const sourceProj = map.value.getView().getProjection();
// @ts-ignore
const length = getLength(line, { projection: sourceProj });
let output;
if (length > 100) {
output = Math.round((length / 1000) * 100) / 100 + " " + "km";
} else {
output = Math.round(length * 100) / 100 + " " + "m";
}
return output;
};
const formatArea = (polygon: any) => {
const area = getArea(polygon, {
projection: "EPSG:4326",
});
let output;
if (area > 10000) {
output =
Math.round((area / 1000000) * 100) / 100 + " " + "km<sup>2</sup>";
} else {
output = Math.round(area * 100) / 100 + " " + "m<sup>2</sup>";
}
return output;
};
const addInteraction = () => {
const type =
measureType == "area" ? GeometryType.POLYGON : GeometryType.LINE_STRING;
drawObj.value = new Draw({
source: source,
type: type,
// condition: mouseOnly,
// freehandCondition: noModifierKeys,
style: new Style({
fill: new Fill({
color: "rgba(255, 255, 255, 0.2)",
}),
stroke: new Stroke({
color: "red",
lineDash: [10, 10],
width: 2,
}),
image: new CircleStyle({
radius: 5,
stroke: new Stroke({
color: "red",
}),
fill: new Fill({
color: "rgba(255, 255, 255, 0.2)",
}),
}),
}),
});
map.value.addInteraction(drawObj.value);
createHelpTooltip();
let listener: any;
drawObj.value.on("drawstart", function (evt: any) {
createMeasureTooltip();
disableMapEvent.value = true;
// set sketch
sketch = evt.feature;
/** @type {module:ol/coordinate~Coordinate|undefined} */
let tooltipCoord: any;
listener = sketch.getGeometry()?.on("change", function (evt: any) {
const geom = evt.target;
let output = "";
if (geom instanceof Polygon) {
output = formatArea(geom);
tooltipCoord = geom.getInteriorPoint().getCoordinates();
} else if (geom instanceof LineString) {
output = formatLength(geom);
tooltipCoord = geom.getLastCoordinate();
}
measureTooltipElement.value.innerHTML = output;
measureTooltip.value.setPosition(tooltipCoord);
});
});
drawObj.value.on("drawend", function () {
disableMapEvent.value = false;
measureTooltip.value.setOffset([0, -7]);
// unset sketch
sketch.dispose();
// unset tooltip so that a new one can be created
measureTooltipElement.value = document.createElement("div");
listener && unByKey(listener);
});
};
const createHelpTooltip = () => {
helpTooltipElement.value?.parentNode?.removeChild(helpTooltipElement.value);
helpTooltipElement.value = document.createElement("div");
helpTooltipElement.value.className = "tooltip hidden";
helpTooltip.value = new Overlay({
id: "helpTooltip",
element: helpTooltipElement.value,
offset: [15, 0],
zIndex: 10000,
positioning: OverlayPositioning.CENTER_LEFT,
});
map.value.addOverlay(helpTooltip.value);
};
const createMeasureTooltip = () => {
// measureTooltipElement?.parentNode?.removeChild(measureTooltipElement);
measureTooltipElement.value = document.createElement("div");
measureTooltipElement.value.className = "ol-tooltip ol-tooltip-measure";
// measureTooltipElement.setAttribute('class', comStyle['ol-tooltip-measure']);
measureTooltip.value = new Overlay({
oId: "measureTooltip",
element: measureTooltipElement.value,
offset: [0, -15],
zIndex: 10000,
positioning: OverlayPositioning.BOTTOM_CENTER,
});
map.value.addOverlay(measureTooltip.value);
//@ts-ignore
window["measureTooltip"] = measureTooltip.value;
};
addInteraction();
};
/**
* 清除
*/
const clear = () => {
disableMapEvent.value = false;
drawObj.value && map.value.removeInteraction(drawObj.value);
vector && map.value.removeLayer(vector);
const overlays = map.value.getOverlays().getArray();
if (overlays) {
overlays
.filter(
(o: any) =>
o &&
(o.options?.oId === "measureTooltip" || o.getId() === "helpTooltip")
)
.forEach(function (o: any) {
map.value.removeOverlay(o);
});
}
const layers = map.value.getLayers();
if (layers) {
layers
.getArray()
.filter((o: any) => o?.get("name") === "drawLayer")
.forEach(function (o: any) {
o.getSource().clear();
map.value.removeLayer(o);
});
}
drawObj.value = null;
};
const tooltipClick = () => {
};
onMounted(() => {
initMap();
});
defineExpose({
map, //地图
/**
@desc 添加站点
@param [{
stcd:0000,
stnm:"xxxx",
sttp:'',
icon:'xxxx',
lng:number,
lat:number
}
data:[{name:'xx',value:'',unit:''}] //data地图鼠标悬浮展示内容
}]
*/
addStation,
/**
* 缩放 地图
* @param {number} zoom 缩放级别
*/
zoomInOut,
/**
* 地图中心点
* @param {number} center 地图中心点参数格式 [lng,lat]
* @param {number} zoom 缩放级别 15
*/
toCenter,
/**
* 清除地图分析
*/
clear,
/**
* 地图分析
* @param {string} type 分析类型 'area' 面积 'line' 距离
*/
draw,
/**
* 图源切换事件
* @param {string} type 图层类型
*/
imagSourceClick
});
</script>
<style scoped lang="less">
.map-container {
height: 100%;
position: relative;
background: url("/@/assets/map/mapbg.png");
.btn {
position: absolute;
z-index: 10;
top: 20px;
left: 10px;
.btn-item {
background-color: #139eb1;
margin: 0px 10px;
color: #fff;
border: none;
height: 30px;
border-radius: 4px 4px 4px 4px;
}
}
.mouseInfo {
position: absolute;
z-index: 10;
left: 40%;
bottom: 20px;
pointer-events: none;
.btn-item {
margin: 0px 10px;
color: #fff;
border: none;
height: 30px;
border-radius: 4px 4px 4px 4px;
}
}
.map {
height: 100%;
}
.tooltip {
position: absolute;
width: 250px;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.25);
border-radius: 4px 4px 4px 4px;
padding: 5px 12px;
border-top: 4px solid #0fe2ff;
.content-col {
margin-top: 8px;
font-family:
PingFang SC,
PingFang SC;
font-weight: 400;
font-size: 14px;
color: #666666;
line-height: 16px;
text-align: left;
font-style: normal;
text-transform: none;
border-radius: 4px 4px 0px 0px;
}
}
:deep(.ant-popover, .ant-popover-content) {
border-radius: 10px;
}
}
.tyxz {
border-radius: 10px;
.tybox {
cursor: pointer;
padding-left: 5px;
padding-top: 5px;
}
.desc {
text-align: center;
font-size: 12px;
margin-top: 5px;
}
.activeimg {
background: #139eb1;
color: #fff;
}
}
</style>
maptools.vue 文件
<template>
<div>
<el-popover placement="top" :width="160">
<template #reference>
<el-button size="small" type="primary" v-show="mapType=='2D'" > 图源选择 </el-button>
</template>
<el-row :gutter="20" style="width: 280px" class="tyxz">
<el-col :span="12" :class="'tybox ' + (imgSource == 'dz' ? ' activeimg' : '')"
@click="imagSourceClick('dz')">
<img :src="dzs" alt="" width="120" height="120" srcset="" />
<p class="desc">电子</p>
</el-col>
<el-col :span="12" :class="'tybox ' + (imgSource == 'yx' ? ' activeimg' : '')"
@click="imagSourceClick('yx')">
<img :src="yxs" alt="" srcset="" width="120" height="120" />
<p class="desc">影像</p>
</el-col>
</el-row>
</el-popover>
<el-button size="small" type="primary" v-for="item in tools" v-show="item.type.indexOf(mapType) > -1" @click="item.click" :key="item.id">{{ item.name }}</el-button>
</div>
</template>
<script lang="ts" setup>
import dzs from "/@/assets/map/dz.png";
import yxs from "/@/assets/map/yx.png";
const props = defineProps({
map:Object,
mapType:{
type:String,
default:'3D'
}
})
const mapType = ref(props.mapType);
const tools = [
{id:1,name:'点位',type:'3D',click:()=>{measurePoint()}},
{id:2,name:'距离',type:'2D,3D',click:()=>{measureLine()}},
{id:2,name:'高度',type:'3D',click:()=>{measureHeight()}},
{id:3,name:'面积',type:'2D,3D',click:()=>{measureArea()}},
// {id:6,name:"挖坑",type:'3D',click:()=>{console.log('点击了')}},
{id:7,name:"体积",type:'3D',click:()=>{measureVolume()}},
{id:8,name:"清除测量",type:'2D,3D',click:()=>{clear()}}
]
const imgSource = ref<string>("yx");
const imagSourceClick = (type:any) => {
imgSource.value = type;
props.map?.imagSourceClick(type)
};
const measurePoint = () => {
props.map?.draw('point');
}
const measureHeight = () => {
props.map?.draw('height');
}
const measureLine = () => {
props.map?.draw('line');
}
const measureArea = () => {
props.map?.draw('area');
}
const measureVolume = () => {
props.map?.draw('volume');
}
const clear = () => {
props.map?.clear();
}
</script>
<style scoped lang="scss">
.tyxz {
border-radius: 10px;
.tybox {
cursor: pointer;
padding-left: 5px;
padding-top: 5px;
}
.desc {
text-align: center;
font-size: 12px;
margin-top: 5px;
}
.activeimg {
background: #139eb1;
color: #fff;
}
}
</style>
mapMode.vue文件
<template>
<div class="map-mode">
<el-icon class="icon" @click="toCenter"><Promotion /></el-icon>
<el-icon class="icon" @click="zoomIn"><CirclePlusFilled /></el-icon>
<el-icon class="icon" @click="zoomOut"><RemoveFilled /></el-icon>
<span v-if="mapType==='3D'" class="icon D23" @click="changeMapType('2D')">2D</span>
<span v-if="mapType==='2D'" class="icon D23" @click="changeMapType('3D')">3D</span>
</div>
</template>
<script lang="ts" setup>
const props = defineProps({
map:Object,
mapType:{
type:String,
default:'2D'
}
})
const mapType = ref(props.mapType);
const emit = defineEmits(['changeMapType'])
const changeMapType = (type:string) =>{
mapType.value = type;
emit('changeMapType',type)
}
const zoomIn = () =>{
props.map?.zoomInOut(0.5);
}
const zoomOut = () =>{
props.map?.zoomInOut(-0.5);
}
const toCenter = () =>{
props.map?.toCenter([110.105931,22.422299],16);
}
</script>
<style scoped lang="scss">
.map-mode{
display: flex;
flex-direction: column;
align-items: center;
background-color: rgba(31,60,113,.66);
padding: 5px;
border-radius: 5px;
.icon{
font-size: 20px;
cursor: pointer;
margin:10px 0px;
color: #00cdff;
font-weight: bold;
}
.D23{
font-size: 15px;
}
}
</style>```