在前面介紹了通過openlayers
載入dwg
格式的CAD
圖並與網際網路地圖疊加,openlayers
功能很全面,但同時也很龐大,入門比較難,適合於大中型專案中。而在中小型專案中,一般用開源的leaflet比較多, leaflet
小而美,外掛很多。本文介紹如何用Leaflet來載入DWG格式的CAD圖,並在上面做應用開發,如與網際網路地圖疊加顯示等。
Leaflet
是領先的用於移動友好互動式地圖的開源 JavaScript 庫。僅僅重約 39KB 的 JS,它擁有大多數開發者所需要的所有地圖功能。Leaflet
在設計時考慮到了簡單性、效能和可用性。它可以在所有主要的桌面和行動平臺上高效地工作,可以通過大量的外掛進行擴充套件,擁有一個漂亮的、易於使用的、記錄良好的 API,以及一個簡單的、可讀的原始碼。。
Leaflet 官網地址 https://leafletjs.com/
Leaflet 原始碼地址 [https://github.com/Leaflet/
在leaflet中載入CAD圖,需要建立一個由cad建立的座標系。可以由L.CRS.Simple
來進行擴充套件,設定好座標系的範圍、解析度及轉換引數即可。
// 地圖服務物件,呼叫唯傑地圖服務開啟地圖,獲取地圖的後設資料
let svc = new vjmap.Service(env.serviceUrl, env.accessToken)
// 開啟地圖
let mapId = "sys_zp";
let res = await svc.openMap({
mapid: mapId, // 地圖ID
mapopenway: vjmap.MapOpenWay.GeomRender, // 以幾何資料渲染方式開啟
style: vjmap.openMapDarkStyle() // div為深色背景顏色時,這裡也傳深色背景樣式
})
if (res.error) {
// 如果開啟出錯
message.error(res.error)
}
// 獲取地圖範圍
let mapBounds = vjmap.GeoBounds.fromString(res.bounds);
// 建立一個基於CAD圖範圍的座標系
let CadCRS = L.Class.extend({
includes: L.CRS.Simple,
initialize: function (bounds) {
// 當前CAD圖的範圍
this.bounds = bounds;
// 投影
this.projection = L.Projection.LonLat;
// 計算解析度
let r = (256 / Math.abs(this.bounds.getEast() - this.bounds.getWest()));
// 設定轉換引數 一個仿射變換:一組係數a, b, c, d,用於將一個形式為(x, y)的點變換為 (ax + b, cy + d)並做相反的變換
this.transformation = new L.Transformation(r, -r * this.bounds.getWest(), - r, r * this.bounds.getNorth());
}
});
// leaflet中座標是反的,如果要用L.latLng傳入座標的時候要傳[y,x],如果要傳[x,y],官網建議如下方案
// https://leafletjs.com/examples/crs-simple/crs-simple.html
L.XY = function(x, y) {
if (L.Util.isArray(x)) { // When doing XY([x, y]);
return L.latLng(x[1], x[0]);
}
return L.latLng(y, x); // When doing XY(x, y);
};
// 當前CAD地圖範圍
let bounds = new L.LatLngBounds([L.XY(mapBounds.min.toArray()), L.XY(mapBounds.max.toArray())]);
let center = mapBounds.center(); // 地圖中心點
// 建立leaflet的地圖物件
let map = L.map('map', {
// 座標系
crs: new CadCRS(bounds),
attributionControl: false
}).setView(L.XY([center.x, center.y]), 2); // 設定初始中心點和縮放級別
// 如果要用L.latLng設定的話,x,y應寫反進行設定。如
// map.setView(L.latLng([center.y, center.x]), 2);
// 增加一個柵格瓦片圖層
let layer = L.tileLayer(
svc.rasterTileUrl(), // 唯傑地圖服務提供的cad的柵格瓦片服務地址
{
bounds: bounds // 當前CAD地圖範圍
}
).addTo(map);
// 把圖層增加至地圖中
layer.addTo(map);
選擇高亮的實現思路為:響應地圖的點選事件,通過當前位置去後臺查詢當前的實體的資料。通過返回的geojson資料,在前端用leaflet的geoJSON
進行繪製即可。
let highlightLayer; // 高亮圖層
const highlight_ent = async co => {
if (highlightLayer) {
highlightLayer.remove(); // 先刪除之前的高亮圖層
highlightLayer = null;
}
let res = await svc.pointQueryFeature({
x: co[0],
y: co[1],
zoom: map.getZoom(),
fields: ""
}, pt => {
// 查詢到的每個點進行座標處理回撥
return mapPrj.fromMercator(pt);// 轉成cad的座標
})
if (res && res.result && res.result.length > 0) {
let features = [];
for (let ent of res.result) {
if (ent.geom && ent.geom.geometries) {
let clr = vjmap.entColorToHtmlColor(ent.color);
for (let g = 0; g < ent.geom.geometries.length; g++) {
features.push({
type: "Feature",
properties: {
objectid: ent.objectid + "_" + g,
color: clr,
alpha: ent.alpha / 255,
lineWidth: 1,
name: ent.name,
isline: ent.isline,
layerindex: ent.layerindex
},
geometry: ent.geom.geometries[g]
})
}
// 選擇提示
let content = `feature: ${ent.objectid}; layer: ${cadLayers[ent.layerindex].name}; type: ${ent.name}`
message.info({ content, key: "info", duration: 3});
}
}
let data = {
type: "FeatureCollection",
features: features
}
if (data.features.length > 0) {
highlightLayer = L.geoJSON(data, {
style: function (feature) {
const highlightColor = svc.currentMapParam().darkMode ? "#57FFC9" : "#11F";
return {color: highlightColor, fillColor: highlightColor}; // feature.properties.color
}
})
highlightLayer.addTo(map);
}
}
};
// 地圖服務物件,呼叫唯傑地圖服務開啟地圖,獲取地圖的後設資料
let svc = new vjmap.Service(env.serviceUrl, env.accessToken)
// 上傳dwg檔案
const uploadDwgFile = async file => {
message.info("正在上傳圖形,請稍候", 2);
let res = await svc.uploadMap(file); // 上傳地圖
// 輸入圖id
let mapid = prompt("請輸入圖名稱ID", res.mapid);
res.mapid = mapid;
res.mapopenway = vjmap.MapOpenWay.GeomRender; // 幾何渲染,記憶體渲染用vjmap.MapOpenWay.Memory
res.isVector = false; // 使用柵格瓦片
res.style = vjmap.openMapDarkStyle(); // 深色樣式,淺色用openMapDarkStyle
message.info("正在開啟圖形,請稍候,第一次開啟時根據圖的大小可能需要幾十秒至幾分鐘不等", 5);
let data = await svc.openMap(res); // 開啟地圖
if (data.error) {
message.error(data.error)
return;
}
openMap(data);
}
實現思路為:呼叫後臺服務切換CAD圖層,獲取切換的圖層樣式id,修改leaflet柵格圖層的瓦片地址即可。
// 切換圖層
const switchLayer = async layers => {
let res = await svc.cmdSwitchLayers(layers); // 呼叫唯傑服務切換圖層,返回圖層id {layerid: "xxxx"}
let source = layer.getSource();
// 重新設定新的唯傑地圖服務提供的cad的柵格瓦片服務地址
source.setUrl(svc.rasterTileUrl());
// 重新整理
source.refresh();
}
實現思路為:切換圖層時重新新成一個新的div物件,重新new一個新的map對新的div物件相關聯。
const switchToMapId = async (mapId)=> {
let res = await svc.openMap({
mapid: mapId, // 地圖ID
mapopenway: vjmap.MapOpenWay.GeomRender, // 以幾何資料渲染方式開啟
style: vjmap.openMapDarkStyle() // div為深色背景顏色時,這裡也傳深色背景樣式
})
if (res.error) {
// 如果開啟出錯
message.error(res.error)
return;
}
// 獲取地圖範圍
let mapBounds = vjmap.GeoBounds.fromString(res.bounds);
let mapPrj = new vjmap.GeoProjection(mapBounds);
// 建立一個基於CAD圖範圍的座標系
let CadCRS = L.Class.extend({
includes: L.CRS.Simple,
initialize: function (bounds) {
// 當前CAD圖的範圍
this.bounds = bounds;
// 投影
this.projection = L.Projection.LonLat;
// 計算解析度
let r = (256 / Math.abs(this.bounds.getEast() - this.bounds.getWest()));
// 設定轉換引數 一個仿射變換:一組係數a, b, c, d,用於將一個形式為(x, y)的點變換為 (ax + b, cy + d)並做相反的變換
this.transformation = new L.Transformation(r, -r * this.bounds.getWest(), -r, r * this.bounds.getNorth());
}
});
// 當前CAD地圖範圍
let bounds = new L.LatLngBounds([L.XY(mapBounds.min.toArray()), L.XY(mapBounds.max.toArray())]);
let center = mapBounds.center(); // 地圖中心點
// 建立leaflet的地圖物件
map = L.map(createNewMapDivId(), {
// 座標系
crs: new CadCRS(bounds),
attributionControl: false
}).setView(L.XY([center.x, center.y]), 2); // 設定初始中心點和縮放級別
// 如果要用L.latLng設定的話,x,y應寫反進行設定。如
// map.setView(L.latLng([center.y, center.x]), 2);
// 增加一個柵格瓦片圖層
let layer = L.tileLayer(
svc.rasterTileUrl(), // 唯傑地圖服務提供的cad的柵格瓦片服務地址
{
bounds: bounds // 當前CAD地圖範圍
}
).addTo(map);
// 把圖層增加至地圖中
layer.addTo(map);
map.on('click', (e) => message.info({content: `您點選的座標為: ${e.latlng.lng}, ${e.latlng.lat}}`, key: "info", duration: 3}));
}
實現思路為:通過修改後臺樣式,通過返回的樣式名,修改leaflet柵格圖層的瓦片地址即可。
let curIsDarkTheme = true;
const switchToDarkTheme = async () => {
if (curIsDarkTheme) return;
curIsDarkTheme = true;
document.body.style.background = "#022B4F"; // 背景色改為深色
await updateStyle(curIsDarkTheme)
}
const switchToLightTheme = async () => {
if (!curIsDarkTheme) return;
curIsDarkTheme = false;
document.body.style.backgroundImage = "linear-gradient(rgba(255, 255, 255, 1), rgba(233,255,255, 1), rgba(233,255,255, 1))"
await updateStyle(curIsDarkTheme)
}
const updateStyle = async (isDarkTheme) => {
style.backcolor = isDarkTheme ? 0 : 0xFFFFFF;//深色為黑色,淺色為白色
let res = await svc.cmdUpdateStyle(style);
layer.setUrl(svc.rasterTileUrl()) // 唯傑地圖服務提供的cad的柵格瓦片服務地址
}
通過修改CAD地圖後臺樣式資料自定義地圖。
// 更改樣式
const expressionList = [] ;// 表示式陣列
const updateStyle = async (style) => {
let res = await svc.cmdUpdateStyle({
name: "customStyle2",
backcolor: 0,
expression: expressionList.join("\n"),
...style
});
layer.setUrl(svc.rasterTileUrl()); // 唯傑地圖服務提供的cad的柵格瓦片服務地址
}
// 表示式語法和變數請參考
// 伺服器端條件查詢和表示式查詢 https://vjmap.com/guide/svrStyleVar.html
// 伺服器端渲染表示式語法 https://vjmap.com/guide/expr.html
// 修改顏色 紅color.r, 綠color.g, 藍color.b, 透明度color.a,如果輸入了級別的話,表示此級別及以上的設定
const modifyColor = (color, zoom) => {
let result = "";
let z = Number.isInteger(zoom) ? `[${zoom + 1}]` : '';
if ("r" in color) result += `gOutColorRed${z}:=${color.r};`;
if ("g" in color) result += `gOutColorGreen${z}:=${color.g};`;
if ("b" in color) result += `gOutColorBlue${z}:=${color.b};`;
if ("a" in color) result += `gOutColorAlpha${z}:=${color.a};`;
return result;
}
對多個cad圖進行圖層開關裁剪旋轉縮放處理後合併成一個新的cad圖
// 組合成新的圖,將sys_world圖進行一定的處理後,再與sys_hello進行合成,生成新的地圖檔名
let rsp = await svc.composeNewMap([
{
mapid: "sys_world", // 地圖id
// 下面的引數可以根據實際需要來設定,可以對圖層,範圍,座標轉換來進行處理
layers: ["經緯度標註","COUNTRY"], // 要顯示的圖層名稱列表
//clipbounds: [10201.981489534268, 9040.030491346213, 26501.267379, 4445.465999], // 要顯示的範圍
//fourParameter: [0,0,1,0] // 對地圖進行四引數轉換計算
},
{
mapid: "sys_hello"
}
])
if (!rsp.status) {
message.error(rsp.error)
}
// 返回結果為
/*
{
"fileid": "pec9c5f73f1d",
"mapdependencies": "sys_world||sys_hello",
"mapfrom": "sys_world&&v1&&&&0&&&&&&&&&&00A0&&10||sys_hello&&v1&&&&0&&&&&&&&&&&&2",
"status": true
}
*/
實現思路為:通過條件查詢去後臺獲取所有文字的屬性資料和geojson資料,在前端通過leaflet的geojson圖層繪製每個文字的邊框。
let highlightLayer; // 高亮圖層
const queryTextAndDrawBounds = async () => {
if (highlightLayer) {
highlightLayer.remove(); // 先刪除之前的高亮圖層
highlightLayer = null;
}
let queryTextEntTypeId = getTypeNameById("AcDbText"); // 單行文字
let queryMTextEntTypeId = getTypeNameById("AcDbMText"); // 多行文字
let queryAttDefEntTypeId = getTypeNameById("AcDbAttributeDefinition"); // 屬性定義文字
let queryAttEntTypeId = getTypeNameById("AcDbAttribute"); // 屬性文字
let query = await svc.conditionQueryFeature({
condition: `name='${queryTextEntTypeId}' or name='${queryMTextEntTypeId}' or name='${queryAttDefEntTypeId}' or name='${queryAttEntTypeId}'`, // 只需要寫sql語句where後面的條件內容,欄位內容請參考檔案"伺服器端條件查詢和表示式查詢"
fields: "",
limit: 100000 //設定很大,相當於把所有的圓都查出來。不傳的話,預設只能取100條
}, pt => {
// 查詢到的每個點進行座標處理回撥
return mapPrj.fromMercator(pt);// 轉成cad的座標
})
if (query.error) {
message.error(query.error)
} else {
message.info(`查詢到符合的記數條數:${query.recordCount}`)
if (query.recordCount > 0) {
let features = [];
for(var i = 0; i < query.recordCount; i++) {
let bounds = vjmap.getEnvelopBounds(query.result[i].envelop, mapPrj);
let clr = vjmap.entColorToHtmlColor(query.result[i].color); // 實體顏色轉html顏色(
features.push({
type: "Feature",
properties: {
name: "objectid:" + query.result[i].objectid,
color: clr
},
geometry: {
'type': 'Polygon',
'coordinates': [
bounds.toPointArray(),
],
}
})
}
let data = {
type: "FeatureCollection",
features: features
}
if (data.features.length > 0) {
highlightLayer = L.geoJSON(data, {
style: function (feature) {
return {color: "#FF6EA0", fillColor: "#FF6EA0", fillOpacity: 0.4}; // feature.properties.color
}
})
highlightLayer.addTo(map);
}
}
}
}
實現思路為:利用leaflet提供的圖形繪製外掛https://github.com/Leaflet/Leaflet.draw進行圖形繪製。
var editableLayers = new L.FeatureGroup();
map.addLayer(editableLayers);
var MyCustomMarker = L.Icon.extend({
options: {
shadowUrl: null,
iconAnchor: new L.Point(12, 41),
iconSize: new L.Point(25, 41),
iconUrl: './js/leaflet2.0/plugins/images/marker-icon.png'
}
});
var options = {
position: 'topright',
draw: {
polyline: {
shapeOptions: {
color: '#f357a1',
weight: 10
}
},
polygon: {
allowIntersection: false, // Restricts shapes to simple polygons
drawError: {
color: '#e1e100', // Color the shape will turn when intersects
message: '<strong>Oh snap!<strong> you can\'t draw that!' // Message that will show when intersect
},
shapeOptions: {
color: '#bada55'
}
},
circle: false, // Turns off this drawing tool
rectangle: {
shapeOptions: {
clickable: false
}
},
marker: {
icon: new MyCustomMarker()
}
},
edit: {
featureGroup: editableLayers, //REQUIRED!!
remove: false
}
};
var drawControl = new L.Control.Draw(options);
map.addControl(drawControl);
map.on(L.Draw.Event.CREATED, function (e) {
var type = e.layerType,
layer = e.layer;
if (type === 'marker') {
layer.bindPopup('A popup!');
}
editableLayers.addLayer(layer);
});
// 增加高德地圖底圖
let gdlayer;
const addGaodeMap = async (isRoadway) => {
const tileUrl = svc.webMapUrl({
tileCrs: "gcj02",
tileUrl: isRoadway ? [
"https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}"
] :
/* 如果用影像 */
[
"https://webst0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=6&x={x}&y={y}&z={z}",
"https://webst0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}"
],
tileSize: 256,
tileRetina: 1,
tileMaxZoom: 18,
tileShards: "1,2,3,4",
tileToken: "",
tileFlipY: false,
mapbounds: res.bounds,
srs: "EPSG:4527",// 可通過前兩位獲取 vjmap.transform.getEpsgParam(vjmap.transform.EpsgCrsTypes.CGCS2000, 39).epsg
// 因為sys_cad2000這個圖只有6位,沒有帶系。需要在座標轉換前平移下帶系 https://blog.csdn.net/thinkpang/article/details/124172626
fourParameterBefore: "39000000,0,1,0"
})
gdlayer = L.tileLayer(
tileUrl,
{
zIndex: 0
}
);
gdlayer.addTo(map);
// cad座標與高德座標相互轉換範例
let webCo = await cad2webCoordinate(center, false); // cad轉高德
let cadCo = await web2cadCoordinate(webCo, false); // 高德轉cad
console.log(center, webCo, cadCo)
}
let cadEpsg = "EPSG:4544";// cad圖的espg代號
// 增加cad的wms圖層
let wmsUrl = svc.wmsTileUrl({
mapid: mapId, // 地圖id
layers: layer, // 圖層名稱
bbox: '', // bbox這裡不需要
srs: "EPSG:3857", //
crs: cadEpsg,
})
let mapBounds = vjmap.GeoBounds.fromString(res.bounds);
// cad圖座標轉web wgs84座標
const cadToWebCoordinate = async point => {
let co = await svc.cmdTransform(cadEpsg, "EPSG:4326", point);
return co[0]
}
// 增加wms圖層
let wmsLayer = L.tileLayer.wms(wmsUrl, {
attribution: "vjmap.com"
});
wmsLayer.addTo(map);
// cad上面的點座標
let cadPoints = [
vjmap.geoPoint([587464448.8435847, 3104003685.208651,]),
vjmap.geoPoint([587761927.7224838, 3104005967.655292]),
vjmap.geoPoint([587463688.0280377, 3103796743.3798513]),
vjmap.geoPoint([587760406.0913897, 3103793700.1176634])
];
// 在網際網路圖上面拾取的與上面的點一一對應的座標(wgs84座標)
let webPoints = [
vjmap.geoPoint([116.48476281710168, 39.96200739703454]),
vjmap.geoPoint([116.48746772021137, 39.96022062215167]),
vjmap.geoPoint([116.48585059441585, 39.9588451134361]),
vjmap.geoPoint([116.48317418949145, 39.960515760972356])
]
// 通過座標引數求出四引數
let epsg3857Points = webPoints.map(w => vjmap.geoPoint(vjmap.Projection.lngLat2Mercator(w)));
let param = vjmap.coordTransfromGetFourParamter(epsg3857Points, cadPoints , false); // 這裡考慮旋轉
let fourparam = [param.dx, param.dy, param.scale, param.rotate]
// wms圖層地址
const getCadWmsUrl = (transparent) => {
let wmsUrl = svc.wmsTileUrl({
mapid: mapId, // 地圖id
layers: layer, // 圖層名稱
bbox: '', // bbox這裡不需要
fourParameter: fourparam,
transparent: transparent,
backgroundColor: 'rgba(240, 255, 255)' // 不透明時有效
})
return wmsUrl
}
let mapBounds = vjmap.GeoBounds.fromString(res.bounds);
let cadPrj = new vjmap.GeoProjection(mapBounds);
// cad圖座標轉wgs84座標
const cadToWebCoordinate = point => {
// 再呼叫四引數反算求出web的座標
let mkt = vjmap.coordTransfromByInvFourParamter(vjmap.geoPoint(point), param);
return vjmap.Projection.mercator2LngLat(mkt);
}
// wgs84轉cad圖座標
const webToCadCoordinate = point => {
let mkt = vjmap.Projection.lngLat2Mercator(vjmap.geoPoint(point));
return vjmap.coordTransfromByFourParamter(mkt, param)
}
let VisibleBounds = mapBounds.scale(0.4);
let pt1 = cadToWebCoordinate([VisibleBounds.min.x, VisibleBounds.min.y])
let pt2 = cadToWebCoordinate([VisibleBounds.min.x, VisibleBounds.max.y])
let pt3 = cadToWebCoordinate([VisibleBounds.max.x, VisibleBounds.max.y])
let pt4 = cadToWebCoordinate([VisibleBounds.max.x, VisibleBounds.min.y])
// 計算出cad的範圍
let bounds = vjmap.GeoBounds.fromDataExtent([pt1, pt2, pt3, pt4])
let wmsLayer;
const addWmsLayer = async (transparent)=> {
removeWmsLayer();
let wmsUrl = getCadWmsUrl(transparent);
wmsLayer = L.tileLayer.wms(wmsUrl, {
attribution: "vjmap.com"
});
wmsLayer.addTo(map);
}
const removeWmsLayer = ()=> {
if (!wmsLayer) return;
wmsLayer.remove();
wmsLayer = null;
}
可點選 https://vjmap.com/demo/ 線上體驗上面功能
如果需要用leaflet來載入CAD圖進行開發,請參考範例 https://vjmap.com/demo/
如果需要用openlayers來載入CAD圖進行開發,請參考範例 https://vjmap.com/demo/
如果需要用maptalks來載入CAD圖進行開發,請參考範例 https://vjmap.com/demo/
如何基於vue3來開發Leaflet應用,可檢視此開原始碼 https://github.com/vue-leaflet/vue-leaflet
如何基於vue2來開發Leaflet應用,可檢視此開原始碼 https://github.com/vue-leaflet/Vue2Leaflet