N_GIS 核心知识

N_GIS

WebGIS 数据可视化开发

Openlayers

npm 命令引入 OpenLayers 地图引擎

https://openlayers.org/ API文档 官方案例库

Quick Start

1.安装

Openlayers的npm包名为ol

npm install ol

CDN

<script src="https://cdn.jsdelivr.net/npm/ol@v7.1.0/dist/ol.js"></script>
<script src="https://lib.baomitu.com/openlayers/7.4.0/dist/ol.js"></script>

2.基本地图

import Map from 'ol/Map'
import View from 'ol/View'
import { Tile as TileLayer } from 'ol/layer'
import { XYZ } from 'ol/source'
// fromLonLat方法能将坐标从经度/纬度转换为其他投影
import { fromLonLat } from 'ol/proj'
// 拖拽旋转
import { defaults as defaultInteractions, DragRotateAndZoom } from 'ol/interaction'
 
init () {
 
// 高德 图层
// const tileLayer = new Tile({
//   source: new XYZ({
//     url: 'https://webrd01.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}'
//   })
// })
 
// 天地图 图层 "http://t0.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=xxxxxxxxxxx";
 
  var tileLayer = new TileLayer({
		source: new XYZ({
			url: "http://t0.tianditu.gov.cn/DataServer?T=vec_w&x={x}&y={y}&l={z}&tk=tkxxxxxxxxxxxxx",
		}),
		name: "BaseMap",
	}),
  this.map = new Map({
  controls:[],//控件
  layers: [tileLayer],
  view: new View({
	center: ol.proj.fromLonLat([113.7064,23.29071]), // 地图中心点
	zoom: 15, // 缩放级别
	minZoom: 0, // 最小缩放级别
	maxZoom: 18, // 最大缩放级别
	constrainResolution: true// 因为存在非整数的缩放级别,所以设置该参数为true来让每次缩放结束后自动缩放到距离最近的一个整数级别,这个必须要设置,当缩放在非整数级别时地图会糊
  }),
  target: this.$refs.olMap, // DOM容器
  interactions: defaultInteractions().extend([new DragRotateAndZoom()]) // 按住shit旋转
})
}
 

for cdn

var url = "http://t0.tianditu.gov.cn/img_w/wmts?" +"SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles" +
		"&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=xxxxxxxxxxxxxxx";
var layer =new ol.layer.Tile({
  source: new ol.source.XYZ({
	  url: url,
  }),
  name: "BaseMap",
})
const map = new ol.Map({
  target: 'mapDiv',
  layers: [layer],
  view: new ol.View({
	  center: ol.proj.fromLonLat([113.7064,23.29071]), // 地图中心点
	  zoom: 15, // 缩放级别
	  minZoom: 0,
	  maxZoom: 30,
	  constrainResolution: true
  }),
});
// 绑定事件
var clickHandler = function (event) {
  var coordinate = event.coordinate;
  var lonLat = ol.proj.toLonLat(coordinate);
  document.getElementById("tip").textContent = '坐标:[' +lonLat + ']'
};
map.on('click', clickHandler);

3.加载WMS

import TileWMS from 'ol/source/TileWMS.js';
 
var wms = new TileLayer({
source: new TileWMS({
      url : "http://121.32.129.19:9977/geoserver/ows",
      crossOrigin: 'anonymous',
      attributions: '自定义图层 ',
      params :{
        layers: 'sny:zcgy_1_ex',
        Service: 'WMS',
        Request: 'GetMap',
        Version: '1.1.1',
        format: 'image/png',
        transparent:true,
      },
    serverType: 'mapserver',
  }),
});
this.map.addLayer(wms)

for cdn

var wmsLayer = new ol.layer.Tile({
	source: new ol.source.TileWMS({
		url:"http://192.168.40.112:8080/geoserver/ows",
		crossOrigin: 'anonymous',
		attributions: ' ',
		params :{
			layers: 'gbnt:xxxxxx',
			Service: 'WMS',
			Request: 'GetMap',
			Version: '1.1.1',
			format: 'image/png',
			transparent:true,
		},
		serverType: 'mapserver',
	}),
});
map.addLayer(wmsLayer)
封装
/**
 * WMS图层配置函数
 * @param {Object} config - 配置对象
 * @param {string} config.name - 图层名称
 * @param {string} config.url - 服务地址
 * @param {string} config.styles - 样式名称
 * @param {string} config.cql_filter - CQL过滤器
 * @param {Object} config.params - 额外的WMS参数
 * @param {string} config.authorization - 认证头
 * @param {Object} config.layerOptions - 图层选项
 * @param {Object} config.sourceOptions - 数据源选项
 * @returns {ol.layer.Tile} OpenLayers瓦片图层
 */
function getCustomWMSLayer(config) {
    // 默认配置
    const defaultConfig = {
        url: "http://192.xxx.xxx.xxx:8080/geoserver/ows",
        authorization: 'Basic xxxxx=',
        transparent: true,
        version: '1.1.1',
        service: 'WMS',
        request: 'GetMap',
        serverType: 'geoserver',
        crossOrigin: 'anonymous',
        params: {},
        layerOptions: {},
        sourceOptions: {}
    };
    // 合并配置
    const mergedConfig = Object.assign({}, defaultConfig, config);
    // 构建WMS参数
    const params = {
        layers: mergedConfig.name,
        Service: mergedConfig.service,
        Request: mergedConfig.request,
        Version: mergedConfig.version,
        transparent: mergedConfig.transparent
    };
    
    // 添加可选参数
    if(mergedConfig.styles) {
        params.STYLES = mergedConfig.styles;
    }
    if(mergedConfig.cql_filter) {
        params.CQL_FILTER = mergedConfig.cql_filter;
    }
    
    // 合并额外参数
    Object.assign(params, mergedConfig.params);
    
    // 创建数据源
    const source = new ol.source.TileWMS(
        Object.assign({
            url: mergedConfig.url,
            crossOrigin: mergedConfig.crossOrigin,
            attributions: mergedConfig.attributions || ' ',
            params: params,
            serverType: mergedConfig.serverType,
            // 自定义tileLoadFunction来处理认证
            tileLoadFunction: function(tile, src) {
                const customHeaders = mergedConfig.headers || {};
                const authHeader = mergedConfig.authorization ? 
                    {'Authorization': mergedConfig.authorization} : {};
                
                fetch(src, {
                    headers: Object.assign({}, authHeader, customHeaders)
                })
                .then(response => response.blob())
                .then(blob => {
                    const url = URL.createObjectURL(blob);
                    tile.getImage().src = url;
                })
                .catch(err => console.error('Tile load error:', err));
            }
        }, mergedConfig.sourceOptions)
    );
    
    // 创建图层
    const layer = new ol.layer.Tile(
        Object.assign({
            source: source
        }, mergedConfig.layerOptions)
    );
    
    return layer;
}
 
 
map.addLayer(getCustomWMSLayer({name: 'xxx:xxxxxxxxx', styles:'green'}))

4.加载 WMTS

WMTS 的方式

WMTS方式遵从标准的OGC规范 参考官网的示例 https://openlayers.org/en/latest/examples/wmts.html

 /**
 * 创建支持自定义请求头(如 Authorization)的 GeoServer WMTS 图层
 * 
 * @param {Object} config - 配置对象
 * @param {string} [config.url='http://192.168.40.112:8080/geoserver/gwc/service/wmts'] - WMTS 服务地址
 * @param {string} config.layer - 图层名称,例如 'sny:sz_ns_lzy_0828'
 * @param {string} [config.matrixSet='EPSG:3857'] - 矩阵集
 * @param {string} [config.format='image/png'] - 图像格式
 * @param {string} [config.style=''] - 样式名称
 * @param {number} [config.maxZoom=18] - 最大缩放级别(0 ~ maxZoom)
 * @param {Object} [config.headers={}] - 自定义请求头,例如 { 'Authorization': 'Basic xxx' }
 * @param {Object} [config.layerOptions={}] - ol.layer.Tile 的额外选项
 * @param {Object} [config.sourceOptions={}] - ol.source.WMTS 的额外选项
 * @returns {ol.layer.Tile}
 */
function getWMTSLayer(config) {
	const defaultConfig = {
		url: 'http://192.168.40.112:8080/geoserver/gwc/service/wmts',
		layer: '',
		matrixSet: 'EPSG:3857',
		format: 'image/png',
		style: '',
		maxZoom: 18,
		headers: {},
		layerOptions: {},
		sourceOptions: {}
	};
	const merged = Object.assign({}, defaultConfig, config);
 
	if (!merged.layer) {
		throw new Error('Missing required parameter: layer');
	}
 
	// 投影与切片方案
	const projection = ol.proj.get('EPSG:3857');
	const projectionExtent = projection.getExtent();
	const size = ol.extent.getWidth(projectionExtent) / 256;
	const resolutions = [];
	const matrixIds = [];
 
	const zoomLevels = merged.maxZoom + 1; // 0 到 maxZoom 共 maxZoom+1 级
	for (let z = 0; z < zoomLevels; ++z) {
		resolutions[z] = size / Math.pow(2, z);
		// 注意:GeoServer RESTful 模式通常使用 "EPSG:3857:z" 格式的 matrixId
		matrixIds[z] = merged.matrixSet + ':' + z;
	}
 
	// 构建 RESTful URL 模板
	const extension = merged.format.split('/').pop(); // e.g., 'png' or 'jpeg'
	const templateUrl = `${merged.url}/${merged.layer}/${merged.style}/{TileMatrix}/{TileCol}/{TileRow}.${extension}`;
	// 创建 WMTS 数据源
	const wmtsSource = new ol.source.WMTS(
		Object.assign({
			url: templateUrl,
			layer: merged.layer,
			matrixSet: merged.matrixSet,
			format: merged.format,
			projection: projection,
			tileGrid: new ol.tilegrid.WMTS({
				origin: ol.extent.getTopLeft(projectionExtent),
				resolutions: resolutions,
				matrixIds: matrixIds
			}),
			style: merged.style,
			wrapX: true,
			crossOrigin: 'anonymous',
			// 支持自定义请求头的关键:重写 tileLoadFunction
			tileLoadFunction: function(tile, src) {
				fetch(src, {
					headers: merged.headers
				})
				.then(response => {
					if (!response.ok) {
						throw new Error(`HTTP error! status: ${response.status}`);
					}
					return response.blob();
				})
				.then(blob => {
					const url = URL.createObjectURL(blob);
					tile.getImage().src = url;
				})
				.catch(error => {
					console.error('Failed to load WMTS tile:', error);
					tile.setState(ol.TileState.ERROR);
				});
			}
		}, merged.sourceOptions)
	);
	const layer = new ol.layer.Tile(
		Object.assign({
			source: wmtsSource
		}, merged.layerOptions)
	);
	
	
	return layer;
}
 
var layer_Debug = new ol.layer.Tile({
	source: new ol.source.TileDebug({
		projection: projection,
		tileGrid: wmtsSource.getTileGrid(),
	})
})
map.addLayer(layer_Debug)
 
 
//项目影像图
var layer = getWMTSLayer({
    layer: 'xxx:xxxxxxxxxxxxxxx',
    headers: {
        'Authorization': 'Basic d2ViX3VzZXI6WUpBS0wzUE92MHN2d3JIb2Y='
    },
    maxZoom: 25
});
map.addLayer(layer);
 
 

请求一直<ExceptionText>Unknown TILEMATRIX XX</ExceptionText> 异常

主要是以下三个参数有问题.

XYZ 的方式
// geoserver wmts
var url =
	"http://192.168.40.112:8080/geoserver/gwc/service/wmts?" +
	"SERVICE=WMTS&REQUEST=GetTile&VERSION=1.1.1&LAYER=sny:ss_zhnysfjd_180_planning"
	+"&STYLE=&TILEMATRIXSET=EPSG:3857&FORMAT=image/png" 
	+"&TILEMATRIX=EPSG:3857:{z}&TILEROW={y}&TILECOL={x}";
var wmtsLayer = new ol.layer.Tile({
	source: new ol.source.XYZ({//WMTS使用的数据源类型为ol.source.WMTS,XYZ方式使用的数据源类型为ol.source.XYZ;
		url: url,
	}),
	name: "wmts",
});
 
//谷歌 影像图
var layer = new ol.layer.Tile({
	source: new ol.source.XYZ({
		url: 'https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}' 
	})
});
 
//天地图影像
var url = "http://t0.tianditu.gov.cn/img_w/wmts?" +
	"SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles" +
	"&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=c94ca7ff1fda359919b7132d1307ed62";
var layer =new ol.layer.Tile({
	source: new ol.source.XYZ({
		url: url,
	}),
	name: "BaseMap",
})
 
 
 

离线XYZ瓦片

import Map from 'ol/Map.js';
import XYZ from 'ol/source/XYZ.js';
 
import TileLayer from 'ol/layer/Tile.js';
 
const xyzSource = new XYZ({
  crossOrigin: 'anonymous', tileUrlFunction: (zxy) => {
    const [z, x, y] = zxy;
    const yn = Math.pow(2, z) - y - 1;
    //通过 WIM.getStaticResourcesBase64 方法
    //瓦片路径地址为: map/{id}/{format.replaceAll("{x}",x).replaceAll("{y}",yn).replaceAll("{z}",z)}
    //注意 format 为 x y z 占位符要替换
    const formatUrl = format.replaceAll("{x}",x).replaceAll("{y}",yn).replaceAll("{z}",z)
    const dataURL = WIM.getStaticResourcesBase64(`map/${id}/${formatUrl}`+) || '';
    return 'data:image/png;base64,' + dataURL
  }
})
const offlineMapLayer = new TileLayer({
  zIndex: 3,
  source: xyzSource
});
this.map.addLayer(offlineMapLayer);

基本概念 Basic Concepts

https://openlayers.org/doc/tutorials/concepts.html

Map OpenLayers 的核心组件是 Map (来自 ol/Map 模块)。它会被渲染到一个目标容器中(例如包含地图的网页上的 div 元素)。所有地图属性都可以在构造时进行配置,也可以使用 setter 方法进行配置,例如 setTarget() 方法。

View The map is not responsible for things like center, zoom level and projection of the map. Instead, these are properties of a ol/View instance. Map 并不负责处理地图的中心点、缩放级别和投影等信息。这些是 ol/View 实例的属性。

import View from 'ol/View.js';
 
map.setView(new View({
         center: ol.proj.fromLonLat([113.7064,23.29071]), // 地图中心点
  zoom: 2,
}));

Layer A layer is a visual representation of data from a source. OpenLayers has four basic types of layers:

  1. ol.layer.Tile:用于显示基于图块的地图数据的图层,例如瓦片地图服务(TMS)或 Web Map Service(WMS)。
  2. ol.layer.Image:用于显示基于图像的地图数据的图层,例如基于图像的地图服务。
  3. ol.layer.Vector:用于显示矢量数据的图层,可以包含点、线、面等几何要素。
  4. ol.layer.Group:用于组合多个图层的图层组,可以同时管理多个子图层。
  5. ol.layer.Heatmap:用于显示热力图的图层,可以可视化密度数据。
  6. ol.layer.VectorTile:用于显示矢量切片的图层,可以提供高性能的矢量数据渲染。
  7. ol.layer.ImageStatic:用于显示静态图像的图层,可以显示单个静态图像。
  8. ol.layer.TileVector:用于显示矢量切片的图层,将矢量数据加载为图块以提供渲染效率。
  9. ol.layer.VectorImage:用于显示矢量数据的图层,使用矢量图像渲染引擎进行高性能渲染。
  10. ol.layer.TileLayer:用于显示图块数据的图层,与 ol.layer.Tile 类似,但是具有一些额外的功能。

Source Layer 的数据来源于 ol/source/source 及其子类; VectorSource 是矢量类型的 source, 往 Source 中添加 Feature 数据, 类似 geojson

  1. ol.source.Vector:用于矢量数据的数据源,可以包含点、线、面等几何要素。
  2. ol.source.TileWMS:用于加载基于 Web Map Service(WMS)的图块数据的数据源。
  3. ol.source.ImageWMS:用于加载基于 WMS 的图像数据的数据源。
  4. ol.source.OSM:用于加载 OpenStreetMap 数据的数据源。
  5. ol.source.XYZ:用于加载 XYZ 格式的图块数据的数据源。
  6. ol.source.TileJSON:用于加载基于 TileJSON 规范的图块数据的数据源。
  7. ol.source.ImageStatic:用于加载静态图像的数据源。
  8. ol.source.GeoJSON:用于加载 GeoJSON 格式的矢量数据的数据源。
  9. ol.source.Cluster:用于对矢量数据进行聚类的数据源。
  10. ol.source.TileVector:用于加载矢量切片的数据源。
var source = new VectorSource();
const vector = new VectorLayer({
  source: source,
  style: {
      'fill-color': 'rgba(255, 255, 255, 0.2)',
      'stroke-color': '#ffcc33',
      'stroke-width': 2,
      'circle-radius': 7,
      'circle-fill-color': '#ffcc33',
  },
})
 
//线
let lineFeature  = new Feature({
    geometry: new LineString(
        fromLonLats([
            [ 113.70469925805553, 23.29105249899534 ], 
            [ 113.70475287618535, 23.290352873226524 ],
        ])
    )
});
source.addFeature(lineFeature)
 
//点
const marker = new Point({
    geometry: fromLonLat([ 113.70469925805553, 23.29105249899534 ]),
});
source.addFeature(marker)

二次封装库

基于 openlayers 封装好的js库

React-OpenLayers:React-OpenLayers 是一个基于 OpenLayers 的 React 库,提供了一组 React 组件,使你可以方便地在 React 应用程序中使用 OpenLayers。它提供了针对地图、图层、要素和交互等的封装组件,简化了与 OpenLayers 的集成和操作。

Vue-OpenLayers:Vue-OpenLayers 是一个基于 OpenLayers 的 Vue 库,提供了一组 Vue 组件,用于在 Vue 应用程序中使用 OpenLayers。它提供了包括地图、图层、要素和交互等的封装组件,使你可以更轻松地在 Vue 项目中集成和使用 OpenLayers。

Vue-OpenLayers

要素绘制

绘制点线面

//要素对象 点
 const point = new ol.Feature({
  geometry: new ol.geom.Point( ol.proj.fromLonLat([data.longitude, data.latitude ])),
  type: 'draggable-point'
});
//要素对象 线
 const lineFeature = new ol.Feature({
	geometry: new ol.geom.LineString([ [[x, y], ... ] ]),
	type: 'line'
});
//要素对象 面
const lonLatCoords = [
  [ 113.5091696577307, 22.643547651857588 ], 
  [ 113.51008677217058, 22.64323079278843 ],
  [ 113.50991514841571, 22.642606974359865 ], 
  ...
]
const epsg3857Coords = lonLatCoords.map(coord => ol.proj.fromLonLat(coord));
const vectorSource = new ol.source.Vector()
const feature = new ol.Feature({
	geometry: new ol.geom.Polygon([epsg3857Coords ])
});
 
//要素源
const vectorSource_machine = new ol.source.Vector();
vectorSource_machine.addFeature(point);
// 等价
//const vectorSource_machine = new ol.source.Vector({ features: [point] });
 
//图层 向量图层
const vectorLayer_machine = new ol.layer.Vector({
  source:  vectorSource_machine,
  style: new ol.style.Style({//图层样式
	image: new ol.style.Circle({
	  radius: 7,
	  fill: new ol.style.Fill({color: 'red'}),
	  stroke: new ol.style.Stroke({
		color: 'white', width: 2
	  })
	}),
	stroke: new ol.style.Stroke({
	  color: 'blue', width: 3
	}),
	fill: new ol.style.Fill({
	  color: 'rgba(0, 0, 255, 0.3)'
	})
  })
});
map.addLayer(vectorLayer_machine);

点线面编辑

参考示例库: Draw and Modify Features

 
// 创建一个空的矢量图层用于显示路线
const source = new VectorSource();
const vector = new VectorLayer({
    source: source,
    style: {
        'fill-color': 'rgba(255, 255, 255, 0.2)',
        'stroke-color': '#ffcc33',
        'stroke-width': 2,
        'circle-radius': 7,
        'circle-fill-color': '#ffcc33',
    },
})
this.mapInstance.addLayer(vector)
 
 
// 创建一个绘制交互工具用于绘制路线
var draw = new Draw({
    source: source,
    type: 'LineString',// 指定绘制类型为线要素
});
this.mapInstance.addInteraction(draw);
 
// 创建一个修改交互工具用于编辑路线
var modify = new ol.interaction.Modify({
  source: vectorLayer.getSource()
});
 
// 吸附 辅助编辑的
var snap = new Snap({source: source});
this.mapInstance.addInteraction(snap);

确保 Feature 在地图范围内

// 获取要素的几何范围
var featureExtent = lineFeature.getGeometry().getExtent();
 
// 缩放地图以适应要素的几何范围
this.mapInstance.getView().fit(featureExtent, {
  padding: [50, 50, 50, 50], // 可选:添加边缘留白
  maxZoom: 18 // 可选:限制最大缩放级别
});
 

代码

 
import Map from "ol/Map";
import Overlay from "ol/Overlay"
import View from "ol/View";
import { LineString, Point } from "ol/geom";
import Feature from "ol/Feature";
 
import { Tile as TileLayer, Vector as VectorLayer } from "ol/layer";
import ImageLayer from 'ol/layer/Image.js';
import TileWMS from "ol/source/TileWMS.js";
import { Draw, Modify, Snap } from "ol/interaction";
import { OSM, XYZ, Vector as VectorSource, ImageStatic } from "ol/source";
import { Message } from "element-ui";
import { fromLonLat, toLonLat } from 'ol/proj'
 
const toLonLat = ol.proj.toLonLat
const fromLonLat = ol.proj.fromLonLat
function  fromLonLats(lnglats){
    var coordinates = [];
    for (let index = 0; index < lnglats.length; index++) {
        const element = lnglats[index];
        coordinates.push(fromLonLat(element) );
    }
    return coordinates;
}
function toLonLats(coordinates){
    var lnglats = [];
    for (let index = 0; index < coordinates.length; index++) {
        const element = coordinates[index];
        lnglats.push(toLonLat(element) );
    }
    return lnglats;
}
 
export default {
  props: {
    //中心点 车的位置
    lnglat: {
      type: Object,
      required: true,
    },
    defaultAction:{},
    originalNode:{},
    isEditAble:{}
  },
  components:{
    nodeContent
  },
  data() {
    return {
      // isEditAble: true, //是否可编辑路径
      mapInstance: null,
      tdLayer: null,//天地图 图层
 
      showNodes: [],
      //任务线绘制
      curTaskpInstance: null,
      isStartTaskp: false, //是否开始任务中
      recordLnglat: [], //记录任务的路径点
 
      //实时位置
      realMarkInstance: null,
 
      //路径编辑
      pathLayer: null,
      drawInstance: null, //绘制
      editorInstance: null, //修改
      switchPathEditor: false,
      currentPolyline: null,
 
      //路段编辑
      actionLayer: null,
      actionPointLayer: null,
      switchActionEditor: false,
      actionPorintInstances: [],
      curActionPolylineInstance: null,
 
      pathActionData: {
        //"0":{ "gears": 0, "sp_l": 0, "sp_r": 0 }
      },
      curActionDataIndex: 0,
      curActionData: {
        //{ "gears": 0, "sp_l": 0, "sp_r": 0 }
      },
      mousePosition: {},
      sliderMarks: {
        0: 0,
        1: 1,
        2: 2,
        3: 3
      },
      //高精度图层
      configLayers: [
        { _name:"广州-农装所",  _layer: 'sny:gz_nzs_hp'  , visible: true},
        // { _name:"广州-白云-仲恺",  _layer: 'sny:gz_by_zk', visible: true},
        // { _name:"广州-室内-测试图层",  _layer: 'sny:gz_test_sn' , visible: true},
        { _name:"广州-室内-蔬菜所测试室内外一体定位",  _layer: 'sny:gz_scs_dp_sn' , visible: true},
      ],
    };
  },
  created() {
    if(!this.isEditAble){
      this.isEditAble=false
      // this.drawInstance.setActive(false)
    }else{
      this.isEditAble=true
      
    }
    this.curActionData = this.defaultAction;
    // this.time = setInterval(() => {
    //   this.markerCar(this.lnglat.lng,this.lnglat.lat)
    // },1000)
  },
  mounted() {
    this.initAsnyc().then(res=>{
    })
 
  },
  watch: {
    lnglat(newVal,oldVal){
      if(newVal){
        this.lnglatUpdate();
        this.markerCar(this.lnglat.lng,this.lnglat.lat)
      }
    }
    //watch
    // lnglat: function refresh() {
    //   console.log('okk111')
    //   this.lnglatUpdate();
    //   this.markerCar(this.lnglat.lng,this.lnglat.lat)
    // },
 
// originalNode(newVal,oldVal){
// if(newVal){
// console.log(this.originalNode)
// if(this.originalNode.length>0){
// console.log('okkkkk')
// }
// }
// }
  },
  methods: {
    initAsnyc(){
            var self = this;
            window._sny_map = self
            ////////////创建 初始化 地图
            const promise = new Promise(function(resolve,reject){  
            self.$nextTick(()=>{
                var dom = self.$refs.map_container;
                var url = "http://t0.tianditu.gov.cn/img_w/wmts?" +
                    "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles" +
                    "&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=c94ca7ff1fda359919b7132d1307ed62";
                var layer = self.tdLayer = new TileLayer({
                    source: new XYZ({
                        url: url,
                    }),
                    name: "BaseMap",
                })
                var map = self.mapInstance = new Map({
                    target: dom,
                    layers: [layer],
                    controls:[],
                    view: self.initView(),
                });
                map.on('click', function(event) {
                  var pixel = event.pixel;
                  map.forEachFeatureAtPixel(pixel, function(feature, layer) {
                      var call_ = feature.get("onClick")
                      call_ && call_(event)
                  });
                });
                self.initConfigLayers()
                self.createEditorInstance()
                self.markerCar(self.lnglat.lng,self.lnglat.lat)
                if(!self.isEditAble){
                self.closePathEdit()}
                resolve(); 
            })
            ////////tick
            });
            return promise;
        },
        initView(){
          var self = this;
          var view = new View({
            // center: fromLonLat([113.7064,23.29071]), // 地图中心点 数字土壤
            // center: fromLonLat([113.337795, 23.146183]), //广州所
            // center: fromLonLat([113.442650, 23.366700]), //仲恺
            center: fromLonLat([self.lnglat.lng, self.lnglat.lat]),//根据上报点
            zoom: 18, // 缩放级别
            minZoom: 15,
            maxZoom: 30,
            constrainResolution: false
          })
          view.on("change:resolution", e=>{
            let zval = e.target.getZoom()
            if (zval > 18.35) {
              self.tdLayer.setVisible(false)
            }else{
              self.tdLayer.setVisible(true)
            }
          })
          return view;
        },
        initConfigLayers(){
 
          for (let index = 0; index < this.configLayers.length; index++) {
            const element = this.configLayers[index];
            var wms = new TileLayer({
              visible: element.visible,
              source: new TileWMS({
                  url : "https://www.simae.cn/geoserver/ows",
                  // url : "http://192.168.40.112:8080/geoserver/ows",
                  crossOrigin: 'anonymous',
                  attributions: ' ',
                  params :{
                    layers: [element._layer],
                    Service: 'WMS',
                    Request: 'GetMap',
                    Version: '1.1.1',
                    format: 'image/png',
                    transparent: true,
                  },
                  serverType: 'mapserver',
              }),
            });
            wms.set("id",  element._layer)
            this.mapInstance.addLayer(wms)
          }
          
          // this.mapCentreTo(113.337795, 23.146183)
          // 'sny:gz_nzs_hp', 'sny:gz_by_zk', 'sny:gz_test_sn'
        },
        //修改配置图层后,  同步图层的可见性
        syncConfigLayersVisible(){
           var allLayers = this.mapInstance.getAllLayers()
           for (let index = 0; index < this.configLayers.length; index++) {
            const element = this.configLayers[index];
            const layerInstance = allLayers.find(e=>e.get("id") === element._layer);
            layerInstance.setVisible(element.visible)
           }
        },
        mapCentreTo(lng, lat){
          let v = this.mapInstance.getView()
          v.setCenter(fromLonLat([lng, lat]))
        },
         //开始任务 实时路径
        startRenderPath(){
            this.isStartTaskp = true;
        },
        //暂停任务 实时路径
        pauseRenderPath(){
            this.isStartTaskp = false;
        },
        //开始暂停的任务 实时路径
        pauseStartRenderPath(){
            this.isStartTaskp = true;
        },
        //任务结束 实时路径
        endRenderPath(){
            this.isStartTaskp = false;
            this.recordLnglat = [];
            this.realPathLayer.getSource().clear();
        },
    lnglatUpdate() {
      if (this.isStartTaskp) {
        //开始记录
        var lnglats = [this.lnglat.lng, this.lnglat.lat]
        this.recordLnglat.push(lnglats);
        //画出来 实时线
        this.renderRealPath()//渲染
      }
      //更新实时位置
    },
    //初始化 车的位置标记
    markerCar(lng,lat) {
      if(this.marker){
        this.mapInstance.removeOverlay(this.marker)
      }
      let lnglats =fromLonLat([lng,lat])
      let img = document.createElement("img")
      img.src = require('/src/assets/images/mark.png')//图片地址
      this.marker = new Overlay({
        position: lnglats,
        positioning: 'center-center',
        element: img,
        stopEvent:false
      });
      this.mapInstance.addOverlay(this.marker);
    },
    updateMarkerCar() {
      var self = this;
      if (self.realMarkInstance) {
        let lnglats = wgs84togcj02(this.lnglat.lng, this.lnglat.lat);
        self.realMarkInstance.moveTo(lnglats, { duration: duration });
      }
    },
    
//实时路径 渲染
    renderRealPath(){
        var source = this.realPathLayer.getSource()
        // if (source.getFeatures().length > 0){
        //     let lineFeature = source.getFeatures()
        // }else{
        // }
        let path = this.recordLnglat;
        let lineFeature  = new Feature({
            geometry: new LineString(
                fromLonLats(path)
            )
        });
        source.clear()
        source.addFeature(lineFeature)
 
    },
 
    createEditorInstance() {
      var self = this;
      ///////////// 实时 运行路径
      const rvector = new VectorLayer({//规划路线颜色
          source: new VectorSource(),
          style: {
              'fill-color': 'rgba(255, 255, 255, 0.2)',
              'stroke-color': '#FF0000',
              'stroke-width': 2,
              'circle-radius': 7,
              'circle-fill-color': '#FF0000',
          },
      })
      this.realPathLayer = rvector
      this.mapInstance.addLayer(rvector)
    ///////////////////// 路径规划
      var source = new VectorSource();
      const vector = new VectorLayer({
        //规划路线颜色
        source: source,
        style: {
          "fill-color": "rgba(255, 255, 255, 0.2)",
          "stroke-color": "#ffcc33",
          "stroke-width": 2,
          "circle-radius": 7,
          "circle-fill-color": "#ffcc33",
        },
      });
      this.pathLayer = vector;
      this.mapInstance.addLayer(vector);
 
      const actionLayer = new VectorLayer({
        //路段修改 线
        source: new VectorSource(),
        style: {
          "fill-color": "rgba(255,255,255,0.2)",
          "stroke-color": "#00ff00",
          "stroke-width": 2,
          "circle-radius": 7,
          "circle-fill-color": "#00ff00",
        },
      });
      this.actionLayer = actionLayer;
      this.mapInstance.addLayer(actionLayer);
 
      const actionPointLayer = new VectorLayer({
        //路段修改 点
        source: new VectorSource(),
        style: {
          "fill-color": "rgba(255,255,255,0.2)",
          "stroke-color": "#00ff00",
          "stroke-width": 2,
          "circle-radius": 8,
          "circle-fill-color": "#00ff00",
        },
      });
      this.actionPointLayer = actionPointLayer;
      this.mapInstance.addLayer(actionPointLayer);
 
      
 
      //绘制对象
      var draw = new Draw({
        source: source,
        type: "LineString",
      });
      this.drawInstance = draw;
      draw.on("drawend", (event) => {
        setTimeout(() => {
          draw.setActive(false);
          self.$emit("drawend")
          self.switchActionEdit()
        }, 50);
      });
      this.mapInstance.addInteraction(draw);
      //修改对象
      const modify = new Modify({
        source: source,
        type: "LineString",
      });
      this.editorInstance = modify;
      this.mapInstance.addInteraction(modify);
      //
      var snap = new Snap({ source: source });
      this.mapInstance.addInteraction(snap);
    },
    switchPathEdit() {
      this.switchPathEditor = !this.switchPathEditor;
      if (this.switchPathEditor) {
        this.openPathEdit();
      } else {
        this.closePathEdit();
      }
    },
    openPathEdit() {
      this.switchPathEditor = true;
      this.editorInstance.setActive(true);
      if (this.pathLayer.getSource().getFeatures().length > 0) {
        this.drawInstance.setActive(false);
      } else {
        this.drawInstance.setActive(true);
      }
    },
    closePathEdit() {
      this.switchPathEditor = false;
      this.drawInstance.setActive(false); //禁用 绘制和编辑
      this.editorInstance.setActive(false);
    },
 
    cleanEdit() {
      //清除全部
      this.closePathEdit();
      this.actionLayer.getSource().clear();
      this.pathLayer.getSource().clear();
      /********************************** 
let lineFeature  = new Feature({
    geometry: new LineString(
        fromLonLats([
            [ 113.70469925805553, 23.29105249899534 ], 
            [ 113.70475287618535, 23.290352873226524 ],
            [ 113.70506922315131, 23.291047572066148 ],
            [ 113.70514428853305, 23.290397215813968 ],
            [ 113.70546599731199, 23.291037718207264 ],
            [ 113.70586346424653, 23.290745285031775 ],
            [ 113.7063567510409, 23.290577768950797 ]
        ])
    )
});
 
       let pointFeature  = new Feature({
            geometry: new Point(fromLonLat(  [ 113.70475287618535, 23.290352873226524 ]))
        });
        
this.actionLayer.getSource().clear()
// this.actionLayer.getSource().addFeature(lineFeature)
this.actionPointLayer.getSource().addFeature(pointFeature)
 
*******************************/
    },
    ///////////////////// 路段编辑
    switchActionEdit() {
      this.switchActionEditor = !this.switchActionEditor;
      if (this.switchActionEditor) {
        this.openActionEdit();
      } else {
        this.closeActionEdit();
      }
    },
    openActionEdit() {
      if (this.actionPorintInstances.length > 0) {
        //已编辑中
        return;
      }
      var elements = this.pathLayer.getSource().getFeatures();
      if (elements.length <= 0) {
        Message({ message: "未绘制路径", type: "error", duration: 5 * 1000 });
        return;
      }
      this.closePathEdit(); //先禁用 路径编辑
      let self = this;
      var geometry = elements[0].getGeometry();
      var coordinates = geometry.getCoordinates();
      let paths = toLonLats(coordinates);
      this.actionPointLayer.getSource().clear();
      for (let index = 0; index < paths.length; index++) {
        const lnglat = paths[index];
        //路段编辑时的 点
        const pointFeature = new Feature({
          geometry: new Point(fromLonLat(lnglat)),
        });
        pointFeature.set("onClick", (e) => {
          self.renderActionEditPath(index);
        });
        this.actionPointLayer.getSource().addFeature(pointFeature);
        this.pathActionData[index + ""] = JSON.parse(
          JSON.stringify(this.defaultAction)
        );
        this.actionPorintInstances.push(pointFeature);
      }
      // this.mapInstance.addOverLay(this.actionPorintInstances);
      self.renderActionEditPath(0);
      this.switchActionEditor = true;
    },
    closeActionEdit() {
      // this.mapInstance.removeOverLay(this.actionPorintInstances);
      // if(this.curActionPolylineInstance)
      // this.mapInstance.removeOverLay(this.curActionPolylineInstance);
      this.actionLayer.getSource().clear();
      this.actionPointLayer.getSource().clear();
      this.actionPorintInstances = [];
      this.switchActionEditor = false;
    },
    //渲染某一段
    renderActionEditPath(index) {
      var elements = this.pathLayer.getSource().getFeatures();
      var geometry = elements[0].getGeometry();
      var coordinates = geometry.getCoordinates();
      let paths = toLonLats(coordinates);
      if (index < 0 || index + 1 >= paths.length) {
        return;
      }
      let path = [paths[index], paths[index + 1]];
      this.curActionData = this.pathActionData[index + ""];
      this.curActionDataIndex = index;
 
      let lineFeature = new Feature({
        geometry: new LineString(fromLonLats(path)),
      });
      this.actionLayer.getSource().clear();
      this.actionLayer.getSource().addFeature(lineFeature);
    },
    getAsNodes() {
      var elements = this.pathLayer.getSource().getFeatures();
      if (elements.length <= 0) {
        Message({ message: "未绘制路径", type: "error", duration: 5 * 1000 });
        return;
      }
      var geometry = elements[0].getGeometry();
      var coordinates = geometry.getCoordinates();
      let paths = toLonLats(coordinates);
      let ret = this.encodeAsNodes(paths);
      return ret;
    },
    encodeAsNodes(paths) {
      let nodes = new Array(paths.length);
      for (let index = 0; index < paths.length; index++) {
        const element = paths[index];
        nodes[index] = {
          lng: element[0],
          lat: element[1],
          action: element.action,
        };
        //合路径数据
        nodes[index].action = this.pathActionData[index + ""]
          ? this.pathActionData[index + ""]
          : this.defaultAction;
      }
      return nodes;
    },
    complete(){
      // console.log(this.pathActionData)
      // this.closeActionEdit()
      var node=this.getAsNodes()
      var nodes= JSON.parse(JSON.stringify(node).replace(/lng/g,"longitude").replace(/lat/g, "latitude"));
      this.$emit('complete',nodes)
    },
    initPath(nodes){ 
        //加载, 加载后编辑路段, 不能编辑线!
        this.setByNodes(nodes)
    },
    setByNodes(nodes){
      var source = this.pathLayer.getSource();
      //geo
      var lonlats = nodes.map(e=>[e.lng, e.lat])
      
      let lineFeature  = new Feature({
          geometry: new LineString(
              fromLonLats(lonlats)
          )
      });
      source.addFeature(lineFeature)
      //action 
      for (let index = 0; index < nodes.length; index++) {
          const element = nodes[index];
          this.pathActionData[index+''] = element.action?element.action: this.defaultAction
      }
    },
   
  },
};
 

比例尺

/* 比例尺自定义样式 */
.custom-scale-line-inner {
	position: absolute;
	bottom: 10px;
	left: 20px;
	text-align: center;
	border: 2px solid hsl(113, 81%, 58%);
	color: #000000;
	font-size: 14px;
	font-weight: bold;
	padding: 2px 4px;
}
  // 添加线条比例尺
const scaleLine = new ol.control.ScaleLine({
	units: 'metric',      // 使用公制单位
	bar: false,           // 线条样式(非条形)
	steps: 4,             // 4个刻度
	text: true,           // 显示文字
	className: 'custom-scale-line'  // 使用自定义样式类
});
map.addControl(scaleLine);

Geojson 编辑

<!DOCTYPE html>
<html lang="en">
 
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>openlayers </title>
    <style type="text/css">
        #mapDiv{
            /*地图(容器)显示大小*/
            width: 1200px;
            height: 800px;
            
        }
    </style>
</head>
 
<body>
    <div style="display: flex;">
 
        <div id="mapDiv"></div>
      
        <div style="width:188px;">
            <div id="tip">==============</div>
            <button onclick="document.getElementById('fileInput').click()">加载GeoJSON文件</button>
            <input type="file" id="fileInput" accept=".geojson, .json" onchange="loadGeoJSONFile(event)">
            
            <button onclick="toggleInteraction('draw-point')">绘制点</button>
            <button onclick="toggleInteraction('draw-line')">绘制线</button>
            <button onclick="toggleInteraction('draw-polygon')">绘制面</button>
            <button onclick="toggleInteraction('modify')">修改要素</button>
            <button onclick="toggleInteraction('delete')">删除要素</button>
            <button onclick="saveGeoJSON()">保存GeoJSON</button>
        </div>
 
        <textarea id="output" readonly></textarea>
    </div>
    <script crossorigin="anonymous" src="https://lib.baomitu.com/openlayers/7.4.0/dist/ol.js"></script>
    <!-- <script src="http://api.tianditu.gov.cn/api?v=4.0&tk=c94ca7ff1fda359919b7132d1307ed62" type="text/javascript"></script> -->
 
<script type="text/javascript">
    const toLonLat = ol.proj.toLonLat
    const fromLonLat = ol.proj.fromLonLat
    function  fromLonLats(lnglats){
        var coordinates = [];
        for (let index = 0; index < lnglats.length; index++) {
            const element = lnglats[index];
            coordinates.push(fromLonLat(element) );
        }
        return coordinates;
    }
    function toLonLats(coordinates){
        var lnglats = [];
        for (let index = 0; index < coordinates.length; index++) {
            const element = coordinates[index];
            lnglats.push(toLonLat(element) );
        }
        return lnglats;
    }
///////////////////////////////////////////////////////////////////////////
var map;
let drawInteraction, modifyInteraction, selectInteraction;
const vectorSource = new ol.source.Vector();
 
        function initMap (){
             //天地图 卫星图 WMTS  xyz
            var url = "http://t0.tianditu.gov.cn/img_w/wmts?" +
                "SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles" +
                "&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=c94ca7ff1fda359919b7132d1307ed62";
            var baselayer =new ol.layer.Tile({
                source: new ol.source.XYZ({
                    url: url,
                }),
                name: "BaseMap",
            })
            ////////////////// 本地 天地图 瓦片
            const xyzSource = new ol.source.XYZ({
                crossOrigin: 'anonymous', tileUrlFunction: (zxy) => {
                    const [z, x, y] = zxy;
                    const yn = Math.pow(2, z) - y - 1;
                    //瓦片路径地址为: map/{id}/{format.replaceAll("{x}",x).replaceAll("{y}",yn).replaceAll("{z}",z)}
                    return `./440106000000/${z}/${x}/${y}/tile.jpg`
                }
            })
            const offlineMapLayer = new ol.layer.Tile({
                zIndex: 3,
                source: xyzSource
            });
    
            //////////
            var center = [115.01255129,22.8189894]
            center = [115.01236630, 22.81909524]
            center = [113.337668, 23.145967]//广州市天河区卫星图
            center = [113.33203412782674,23.183219636891778]//广州市天河区 边缘
            const map = window.map = new ol.Map({
                target: 'mapDiv',
                layers: [
                    baselayer
                ],
                view: new ol.View({
                    center: ol.proj.fromLonLat(center),
                    zoom: 18, // 缩放级别
                    minZoom: 0,
                    maxZoom: 30,
                    constrainResolution: true
                }),
            });
            // map.addLayer(offlineMapLayer);
            // map.addLayer( 
            //     new ol.layer.Tile({ source: new ol.source.OSM() })
            // )
            map.addLayer( 
                new ol.layer.Vector({
                    source: vectorSource,
                    style: function(feature) {
                        return new ol.style.Style({
                            image: new ol.style.Circle({
                                radius: 7,
                                fill: new ol.style.Fill({ color: 'red' }),
                                stroke: new ol.style.Stroke({ color: 'white', width: 2 })
                            }),
                            stroke: new ol.style.Stroke({
                                color: 'blue',
                                width: 2
                            })
                        });
                    }
                })
            )
        }
       
/////////////////// 编辑要素
  // 统一样式设置
  function getDefaultStyle() {
            return new ol.style.Style({
                image: new ol.style.Circle({
                    radius: 7,
                    fill: new ol.style.Fill({ color: 'red' }),
                    stroke: new ol.style.Stroke({ color: 'white', width: 2 })
                }),
                stroke: new ol.style.Stroke({
                    color: 'blue',
                    width: 2
                }),
                fill: new ol.style.Fill({
                    color: 'rgba(0, 255, 0, 0.3)'
                })
            });
        }
 
        // 加载GeoJSON数据
        function loadGeoJSON(geojson) {
            const features = new ol.format.GeoJSON().readFeatures(geojson, {
                dataProjection: 'EPSG:4326',
                featureProjection: 'EPSG:3857'
            });
            vectorSource.addFeatures(features);
            map.getView().fit(vectorSource.getExtent());
        }
 
        // 切换交互模式
        function toggleInteraction(type) {
            clearInteractions();
 
            switch (type) {
                case 'draw-point':
                    drawInteraction = new ol.interaction.Draw({
                        source: vectorSource,
                        type: 'Point'
                    });
                    map.addInteraction(drawInteraction);
                    break;
 
                case 'draw-line':
                    drawInteraction = new ol.interaction.Draw({
                        source: vectorSource,
                        type: 'LineString'
                    });
                    map.addInteraction(drawInteraction);
                    break;
 
                case 'draw-polygon':
                    drawInteraction = new ol.interaction.Draw({
                        source: vectorSource,
                        type: 'Polygon'
                    });
                    map.addInteraction(drawInteraction);
                    break;
 
                case 'modify':
                    modifyInteraction = new ol.interaction.Modify({
                        source: vectorSource
                    });
                    map.addInteraction(modifyInteraction);
                    break;
 
                case 'delete':
                    selectInteraction = new ol.interaction.Select();
                    map.addInteraction(selectInteraction);
                    selectInteraction.on('select', (e) => {
                        e.selected.forEach(feature => vectorSource.removeFeature(feature));
                    });
                    break;
            }
        }
 
        // 清除所有交互
        function clearInteractions() {
            if (drawInteraction) map.removeInteraction(drawInteraction);
            if (modifyInteraction) map.removeInteraction(modifyInteraction);
            if (selectInteraction) map.removeInteraction(selectInteraction);
        }
 
        // 保存为GeoJSON
        function saveGeoJSON() {
            const geojsonFormat = new ol.format.GeoJSON();
            const features = vectorSource.getFeatures();
            
            const geojson = geojsonFormat.writeFeatures(features, {
                dataProjection: 'EPSG:4326',
                featureProjection: 'EPSG:3857'
            });
 
            console.log('当前GeoJSON数据:', geojson);
            
            // 自动下载文件
            const blob = new Blob([geojson], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'edited-data.geojson';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }
 
    
    // ////////////////////////  加载本地GeoJSON文件
     function loadGeoJSONFile(event) {
            const file = event.target.files[0];
            if (!file) return;
 
            const reader = new FileReader();
            reader.onload = (e) => {
                try {
                    const geojson = JSON.parse(e.target.result);
                    clearMapData(); // 清除现有数据
                    loadGeoJSON(geojson);
                } catch (error) {
                    alert('文件解析失败: ' + error.message);
                }
            };
            reader.readAsText(file);
        }
 
        // 清空地图数据
        function clearMapData() {
            vectorSource.clear();
        }
 
        // 加载GeoJSON对象到地图
        function loadGeoJSON(geojson) {
            try {
                const features = new ol.format.GeoJSON().readFeatures(geojson, {
                    dataProjection: 'EPSG:4326',
                    featureProjection: 'EPSG:3857'
                });
                vectorSource.addFeatures(features);
                map.getView().fit(vectorSource.getExtent());
            } catch (error) {
                alert('GeoJSON加载失败: ' + error.message);
            }
        }
 
    // 初始化
    initMap();
    // 绑定事件
    var clickHandler = function (event) {
        var coordinate = event.coordinate;
        var lonLat = ol.proj.toLonLat(coordinate);
        document.getElementById("tip").textContent = '坐标:[' +lonLat + ']'
    };
    map.on('click', clickHandler);
    //mapCentreTo
    function mapCentreTo(lng, lat){
        let v = map.getView()
        v.setCenter(fromLonLat([lng, lat]))
    }
    </script>
</body>
 
</html>

Leaflet

https://leafletjs.com/

Leaflet 是一个为建设移动设备友好的互动地图,而开发的现代的、开源的 JavaScript 库. 它是由 Vladimir Agafonkin 带领一个专业贡献者团队开发,虽然代码仅有 38 KB,但它具有开发人员开发在线地图的大部分功能.

leaflet 可以通过简单的 Api 快速构建出简单的地图,结合其他的接口(Marker、 Popup、Icon、Polyline、Polygon等)即可快速实现点、线、面的绘制,社区中也有非常丰富的插件,可以低成本的实现诸如热力图、插值、聚合、数据可视化等功能,需要注意一点 leaflet 只能实现 2D 地图.

Cesium JS

https://www.cesium.com/ https://github.com/CesiumGS/cesium API - 文档 示例 - sandcastle.cesium

CesiumJS是一个JavaScript库,用于在没有插件的情况下在web浏览器中创建3D地球仪和2D地图。它使用WebGL进行硬件加速图形,并且是跨平台、跨浏览器的,并针对动态数据可视化进行了调整。 https://cesium.com/learn/cesiumjs-learn/cesiumjs-quickstart/ https://www.cesium.com/learn/cesiumjs-learn/cesiumjs-quickstart/

关键对象概念

  1. Viewer
    • Cesium.Viewer 是CesiumJS的入口点,用于创建和控制整个场景。它提供了许多配置选项,包括地形、影像、相机控制等。
  2. Camera
    • Cesium.Camera 用于控制视 角和场景中的相机位置。它可以用来飞行、旋转、缩放等。
  3. Scene
    • Cesium.Scene 是CesiumJS的核心对象,代表整个3D场景。它包含了所有的图形资源,如几何体、材质、光源等。
    • Cesium.Globe 它代表了一个三维地球模型。支持用户与地球交互,如缩放、旋转、倾斜视图等。允许开发者自定义地球的外观,包括颜色、水面效果、大气效果等。
  4. Entity
    • Cesium.Entity 是一个高层次的、易于使用的对象,用于表示场景中的实体。实体可以包含位置、外观、时间动态等属性。
  5. DataSource
    • Cesium.DataSource 和其子类用于加载和管理数据源,例如CzmlDataSource、GeoJsonDataSource等,这些数据源可以包含多个实体。
  6. ImageryProvider
    • Cesium.ImageryProvider 是一个抽象类,用于提供地图影像。CesiumJS提供了多种实现,如WebMapServiceImageryProvider、WebMapTileServiceImageryProvider等。
  7. TerrainProvider
    • Cesium.TerrainProvider 用于提供地形数据。CesiumJS支持多种地形数据源,如CesiumTerrainProvider、EllipsoidTerrainProvider等。

quickstart (for vite)

https://github.com/CesiumGS/cesium-webpack-example#cesium-webpack-example

依赖

npm create vite@latest hello-cesiumjs -- --template vue
 
### 安装cesium和vite-plugin-cesium
 
# If you’re building your application using a module bundler such as Webpack, Parcel, or Rollup, you can install CesiumJS by running:
npm install cesium
npm install --save-dev @types/cesium
 
# 不装vite-plugin-cesium可能会导致样式问
npm install vite-plugin-cesium
 

vite.config.js

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import cesium from 'vite-plugin-cesium'
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue(), cesium()],
})
 

Cesium + 天地图

登陆账号拿 Cesium tokent https://ion.cesium.com/tokens?page=1

<template>
 <div ref="container"  id="container"  style="width:100%;height:100%;" >
  </div>
</template>
 
<script setup lang="ts" >
import { ref, reactive, onMounted } from "vue";
import * as Cesium from 'cesium';
 
import { Cartesian3, createOsmBuildingsAsync, Ion, Math as CesiumMath, Terrain, Viewer } from 'cesium';
import "cesium/Build/Cesium/Widgets/widgets.css";
 
// Cesium tokent
Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxxxxxx'
 
onMounted(() => {
  var container = ref(null)
  // Initialize the Cesium Viewer in the HTML element 
  const viewer = new Cesium.Viewer("container", {
    terrain: Cesium.Terrain.fromWorldTerrain(),//地形
  });
 
  // 创建天地图的 ImageryProvider (可以理解为地形贴图)
  viewer.imageryLayers.addImageryProvider(
    new Cesium.WebMapTileServiceImageryProvider({
      url:
        "http://t0.tianditu.com/img_w/wmts?service=wmts&request=GetTile&version=1.0.0&LAYER=img"
          +"&tileMatrixSet=w&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}"
          +"&style=default&format=tiles&tk=天地图tk",
      layer: "tdtBasicLayer",
      style: "default",
      format: "image/jpeg",
      tileMatrixSetID: "GoogleMapsCompatible",
    })
  )
  
  // fly to 中国
  viewer.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(103.84, 31.15, 17850000),
    orientation: {
      heading: Cesium.Math.toRadians(348.4202942851978),
      pitch: Cesium.Math.toRadians(-89.74026687972041),
      roll: Cesium.Math.toRadians(10),
    }
  });
 
})
 
</script>
 
<style>
</style>

WMS 加载

var wmslayer = new Cesium.WebMapServiceImageryProvider({
  url:"http://192.168.50.191:8080/geoserver/wms",
  layers:'sny:tdlylx_2022_all',
  parameters: {
	transparent: true,
	format: "image/png",
	styles: "default",
  },
});
viewer.imageryLayers.addImageryProvider(wmslayer)

实例

坐标转换

经纬度到3D坐标

const position = Cesium.Cartesian3.fromDegrees( -113.0744619,23.0503706, 50);

摄像头

//跳转位置
viewer.camera.setView({
    destination: Cesium.Cartesian3.fromDegrees(115.0, 23.0, 10000),
});
// // fly to 中国
viewer.camera.flyTo({
	destination: Cesium.Cartesian3.fromDegrees(103.84, 31.15, 17850000),
	orientation: {
	 heading: Cesium.Math.toRadians(348.4202942851978),
	 pitch: Cesium.Math.toRadians(-89.74026687972041),
	 roll: Cesium.Math.toRadians(10),
	}
});

地形裁剪

ClippingPlaneCollection 是 CesiumJS 中用于定义和管理一组剪裁平面的集合。你可以使用它来对模型、地形或 3D Tiles 进行裁剪。

 
var scene = viewer.scene;
var globe = scene.globe;
 
// 定义裁剪平面集合,用于裁剪地形
var clippingPlanes = new Cesium.ClippingPlaneCollection({
    planes : [
        new Cesium.ClippingPlane(new Cesium.Cartesian3(1.0, 0.0, 0.0), -0.001),  // 裁剪区域的西侧平面
        new Cesium.ClippingPlane(new Cesium.Cartesian3(-1.0, 0.0, 0.0), -0.001), // 裁剪区域的东侧平面
        new Cesium.ClippingPlane(new Cesium.Cartesian3(0.0, 1.0, 0.0), -0.001),  // 裁剪区域的南侧平面
        new Cesium.ClippingPlane(new Cesium.Cartesian3(0.0, -1.0, 0.0), -0.001), // 裁剪区域的北侧平面
        new Cesium.ClippingPlane(new Cesium.Cartesian3(0.0, 0.0, 1.0), -50.0)    // 裁剪区域的底部平面
    ],
    edgeWidth: 1.0,
    edgeColor: Cesium.Color.WHITE
});
 
// 应用裁剪平面到地球对象
globe.clippingPlanes = clippingPlanes;
 
// 设置裁剪区域的位置
var centerLongitude = 113.0;
var centerLatitude = 23.0;
var height = 50.0;  // 高度,以米为单位
 
// 创建平移矩阵,将裁剪平面移动到指定位置
var translation = Cesium.Cartesian3.fromDegrees(centerLongitude, centerLatitude, height / 2);
clippingPlanes.modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(translation);
 
// 设置视图,以便查看裁剪效果
viewer.camera.setView({
    destination: Cesium.Cartesian3.fromDegrees(centerLongitude, centerLatitude, 5000.0),
    orientation: {
        heading: Cesium.Math.toRadians(0.0),
        pitch: Cesium.Math.toRadians(-30.0),
        roll: 0.0
    }
});

面模型

// 定义多边形的坐标
var positions = Cesium.Cartesian3.fromDegreesArray([
    116.391275, 39.90765, // 西南角
    116.414275, 39.90765, // 西北角
    116.414275, 39.93265, // 东北角
    116.391275, 39.93265  // 东南角
]);
// 创建一个多边形实体
var polygon = viewer.entities.add({
    polygon: {
        hierarchy: positions,
        height: 1000, // 高度
        material: Cesium.Color.RED.withAlpha(0.5) // 透明材质
    }
});
// 跳转到多边形的位置
viewer.zoomTo(viewer.entities);

监听点击事件 经纬度

// 添加点击事件监听器
viewer.canvas.addEventListener('click', function (event) {
    // 获取点击位置的笛卡尔坐标(世界坐标)
    var cartesian = viewer.camera.pickEllipsoid(
        new Cesium.Cartesian3(event.clientX, event.clientY), 
        viewer.scene.globe.ellipsoid
    );
    // 如果我们成功获取了位置,则转换为经纬度
    if (cartesian) {
        var cartographic = Cesium.Cartographic.fromCartesian(cartesian);
        var longitude = Cesium.Math.toDegrees(cartographic.longitude).toFixed(2);
        var latitude = Cesium.Math.toDegrees(cartographic.latitude).toFixed(2);
 
        console.log('Longitude: ' + longitude + ', Latitude: ' + latitude);
    } else {
        console.log('未选中地球表面');
    }
}, false);

3dTiles

obj23dtiles

Cesium 官方提供了一个名为 obj23dtiles 的工具,可以将 OBJ 模型转换为 3D Tiles。

npm install -g obj23dtiles

obj23dtiles  --tileset -i BlockABA_c005.obj 
obj23dtiles  --tileset -i BlockABA_c005_90.obj 

加载 3dTiles

<!--
 * @Author: yangfh
 * @Date: 2025-07-24 11
 * @LastEditors: yangfh
 * @LastEditTime: 2025-07-28 10
 * @Description: 
 * 
-->
 
<template>
 <div ref="container"  id="cesiumContainer"  style="width:100%;height:100%;" >
  </div>
</template>
 
<script setup  >
import { ref, reactive, onMounted } from "vue";
import * as Cesium from 'cesium';
import { Cartesian3, createOsmBuildingsAsync, Ion, Math as CesiumMath, Terrain, Viewer } from 'cesium';
import "cesium/Build/Cesium/Widgets/widgets.css";
 
// Cesium tokent
Ion.defaultAccessToken = '..KA-Bo_6D7FsKd_nvie7notXhWDlGD5i0zFzjxuMQFeI'
 
onMounted( async () => {
var container = ref(null)
const viewer = new Cesium.Viewer("cesiumContainer", {
});
 
try {
  
  const classificationTilesetUrl = "http://192.168.20.130/3d/outputTiles/tileset.json";
  const classificationTileset = await Cesium.Cesium3DTileset.fromUrl(
    classificationTilesetUrl,
    /** // 添加调试参数, 注意 classificationType 设置的不对, 将不会显示模型,    **/
    {
      debugShowBoundingVolume: true,  // 显示包围盒
      debugShowContentBoundingVolume: true,
      debugShowViewerRequestVolume: true
    },
 
  );
 classificationTileset.style = new Cesium.Cesium3DTileStyle({
    color: "rgba(255, 0, 0, 0.9)", // 提高不透明度
    show: true
  });
  viewer.scene.primitives.add(classificationTileset);
  classificationTileset.tileFailed.addEventListener((tile, error) => {
    console.error(`加载错误 Tile failed to load: ${tile.content.url}`);
    console.error(`加载错误 Error: ${error}`);
  });
 
 // 定位到瓦片
  viewer.zoomTo(classificationTileset)
    .catch(e => {
      console.log("自动缩放失败,尝试备用位置");
      // 如果自动缩放失败,使用transform中的位置
      const position = Cesium.Cartesian3.fromElements(
        1215031.1400967704, -4736384.220698239, 4081666.9662339143
      );
      viewer.camera.setView({
        destination: position,
        orientation: {
          heading: 0,
          pitch: -Math.PI/4,
          roll: 0
        }
      });
    });
} catch (error) {
  console.error(`加载错误 Error loading tileset: ${error}`);
} 
 
// 添加调试功能
viewer.scene.debugShowFramesPerSecond = true;
viewer.extend(Cesium.viewerCesium3DTilesInspectorMixin);
var scene = viewer.scene;
 
window._scene = scene
// 添加点击事件监听器
viewer.canvas.addEventListener('click', function (event) {
    // 获取点击位置的笛卡尔坐标(世界坐标)
    var cartesian = viewer.camera.pickEllipsoid(
        new Cesium.Cartesian3(event.clientX, event.clientY), 
        viewer.scene.globe.ellipsoid
    );
    if (cartesian) {
        var cartographic = Cesium.Cartographic.fromCartesian(cartesian);
        var longitude = Cesium.Math.toDegrees(cartographic.longitude).toFixed(2);
        var latitude = Cesium.Math.toDegrees(cartographic.latitude).toFixed(2);
        console.log('点击位置 Longitude: ' + longitude + ', Latitude: ' + latitude);
    } else {
        console.log('未选中地球表面');
    }
}, false);
 
 
})
</script>
 
<style>
 
</style>

Mapbox

https://www.mapbox.com/

Mapbox GL JS 是一个 JavaScript 库,它使用 WebGL,以 vector tiles 和 Mapbox styles 为来源,将它们渲染成互动式地图. 它是 Mapbox GL 生态系统的一部分,其中还包括 Mapbox Mobile,它是一个用 C++ 编写的兼容桌面和移动平台的渲染引擎.

高德地图

WMTS加载

<Exception exceptionCode="TileOutOfRange" locator="TILECOLUMN">

XYZ 加载 (EPSG:3857)

https://lbs.amap.com/demo/jsapi-v2/example/thirdlayer/custom-grid-map

const layer = 'sny:ss_zhnysfjd_one_tree_one_code_0_03'
var xyzTileLayer = new AMap.TileLayer({
	getTileUrl:function (x, y, z){
	var url = "http://192.168.40.112:8080/geoserver/gwc/service/wmts?" 
		+"SERVICE=WMTS&REQUEST=GetTile&VERSION=1.1.1&LAYER="+layer
		+"&STYLE=&TILEMATRIXSET=EPSG:3857&FORMAT=image/png" 
		+`&TILEMATRIX=EPSG:3857:${z}&TILEROW=${y}&TILECOL=${x}`;
		return url;
	},
	zIndex: 100
});
this.map.add(xyzTileLayer)

点击获取最近的路径点

document.getElementById('#map').addEventListener('mousemove', (e) => {
    var pixel = new AMap.Pixel(e.offsetX, e.offsetY)
    var lnglat = map.containerToLngLat(pixel)
    //首先拿到 鼠标的经纬度 mousePosition 
    self.mousePosition = lnglat
})
polyline.on('click', function(ev) {
  let path = ev.target.getPath();
  let minPoint = path[0];
  //循环找到 最小距离的路径点
  path.forEach((item, index) => {
	  var distance = AMap.GeometryUtil.distance(item, self.mousePosition);
	  var minSistance = AMap.GeometryUtil.distance(minPoint, self.mousePosition);
	  if (distance < minSistance) {
		  minPoint = item;
		  minIndex = index;
	  }
  })
  console.log('编辑点索引: ',minIndex);
})

判断几何是否重叠

/**
 * @description: 比较路径是否重叠;
 */
isOverlap(data){
	let len = data.length
	let inlen = data.length - 1
	for (let index = 1; index < len; index++) {
		const p1 = data[index-1];
		const p2 = data[index];
		const baseLine = [p1,p2];
		const inForech = index+1;
		//
		for (let j = inForech; j < inlen; j++) {
			const ip1 = data[j];
			const ip2 = data[j+1];
			if(j+1 - (index-1) === inlen){// 末尾相连的
				break;
			}
			const another = [ip1 ,ip2];
			//let __debug__test = index-1 +","+ index +" <===> "+j +", "+(j+1);
			////- console.log(__debug__test+"; loop");
			var isInSide = AMap.GeometryUtil.doesLineLineIntersect(baseLine, another)
			if (isInSide) {
				////- console.log(__debug__test+"; 存在重叠!");
				return true;;
			}
		}
	}
	return false;
}

踩坑

自定义风格 无效?

(()=>{
	const AMAP_WEB_JS_KEY = 'dc9e0eaa3938f485e044808ec824cc6c'
	const AMAP_SECURITY_JS_CODE = 'd00957714b68cff7f4b9d935f02c02e5'
	var center = [114.015492,22.949568];
	var mapDom = document.getElementById("app")
 
	window._AMapSecurityConfig = {
	securityJsCode: AMAP_SECURITY_JS_CODE
	}
	AMapLoader.load({
		key: AMAP_WEB_JS_KEY, 
		version:"2.0",
	}).then((AMap)=>{
		let map = self.map = new AMap.Map(mapDom, { 
			zoom: 13, 
			//layers:[new AMap.TileLayer()],
			mapStyle:'amap://styles/blue',
			zooms: [0, 18],
			viewMode: '2D',
			center: center
		});
		window._map = map
		console.log("--------------完成---, ", map)
	     
	}).catch(e=>{
		console.log("--------------catch---, ",e)
	})
})()
  

mockjs 冲突, f**K

import { mockXHR } from "@/mock/index";
mockXHR()

天地图

WMS 加载

const layer = 'sny:ss_zhnysfjd_180_planning'
var config = {
	version: "1.1.1",	//请求服务的版本
	layers: layer,
	transparent: true,	//输出图像背景是否透明
	styles: "",			//每个请求图层的用","分隔的描述样式
	format: "image/png"	//输出图像的类型
};
var url = "http://192.168.40.112:8080/geoserver/ows"
var wmsLayer = new T.TileLayer.WMS(url, config);
this.map.addLayer(wmsLayer); 

WMTS 加载

XYZ方式

/// WMTS XYZ
var imageURL = "http://192.168.40.112:8080/geoserver/gwc/service/wmts?" 
	+"SERVICE=WMTS&REQUEST=GetTile&VERSION=1.1.1&LAYER="+layer
	+"&STYLE=&TILEMATRIXSET=EPSG:3857&FORMAT=image/png" 
	+`&TILEMATRIX=EPSG:3857:{z}&TILEROW={y}&TILECOL={x}`;
var lay = new T.TileLayer(imageURL, {minZoom: 1, maxZoom: 30});
this.map.addLayer(lay);

超出范围会导致GeoServer响应异常XML, 导出天地图渲染空白网格. 可参考 http://lbs.tianditu.gov.cn/api/js4.0/class.html 图层类 TileLayerOptions 的 bounds 属性设置指定范围内显示瓦片

属性类型默认值说明
minZoomnumber0此图层的最低缩放级别。
maxZoomnumber18此图层的最高缩放级别。
errorTileUrlstring""当没有瓦片时所显示的错误图片地址。
opacitynumber1.0设置图层的透明度(0.0-1.0)。默认值为 1.0不透明。
zIndexnumbernull图层的显示顺序。
boundsLngLatBoundsnull设置指定范围内显示瓦片。