先看效果
主要功能如下:
- 测量
- 图源更换
- 放大缩小
- 地图添加点
- 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-ignoreconst 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 sketchsketch = 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 sketchsketch.dispose();// unset tooltip so that a new one can be createdmeasureTooltipElement.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-ignorewindow["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>```