离线地图最终解决方案
前言
能找到这个帖子的朋友应该是公司要求做离线地图,但是没了解过的吧,我前一段也是公司要求做离线地图但是我没了解过,我就去搜了很多文章,找了很多方案,最后和leader定下来了两个方案,一个是使用一张固定缩放的图片,然后将像素转化为px来做撒点的效果,另一个就是使用瓦片地图框架openlayers
来实现瓦片地图,后文将一一介绍两个方法。
- 方法1:用一张固定缩放的图片将像素转化成px来做撒点的效果
- 方法2:用地图框架
openlayers
来实现瓦片地图
1.使用openlayers来实现瓦片地图
为什么使用
openlayers
而不是使用leaflet
,原因是我找到的教程很多是openlayers
的,我看leaflet
的很多功能都是社区做的,这样会增大我的学习量,所以我没有选
首先解决地图问题
如果你要用原版地图的话,就可以直接使用地图下载工具瓦片地图下载工具,这只支持你下载原版地图,还有就是每个地图公司的坐标系可能存在不一样,本文是用腾讯地图来做的,为什要用腾讯一会儿说。
为什么要用腾讯地图?
因为腾讯地图有一个东西叫做自定义地图样式的工具,据网上帖子来说,你可以通过控制台去抓取到你自定义样式的地图的瓦片文件,然后用一些工具去批量抓取,但是实测百度地图更新了,抓不到了就很烦!
- 用上面的那个地图下载工具下载的地图格式有点问题,你需要给文件夹改成下面这样的格式
地图存放到服务器
如果给本地搞个
Tomcat
什么的起个服务会很麻烦,这里也是用一个非常快捷的办法直接解决
//首先先去下载 npm install httpserver
//上一步下载完毕后,在你瓦片的分级目录里面打开cmd直接启动服务 http-server
下面会输出一堆东西,不用管他,你只需要知道那个是你的
ip+端口
就行了,将你的ip+端口
复制走,等下要用
使用openlayers加载离线地图,用Vue项目为例
//小声bb : 其实到这里下面的步骤基本就是复制另一个帖子了 这个帖子也给了我很多帮助
安装openlayers
npm install ol
//或者直接用js引入 <script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.8.1/build/ol.js"></script> <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@master/en/v6.8.1/css/ol.css">
复制下面代码
<template>
<div style="width: 100%;height: 100%">
<div class="map" id="map"></div>
<el-card id="popup" class="popup">
<div class="popupContainer"></div>
</el-card>
</div>
</template>
<script>
import 'ol/ol.css';
import Map from 'ol/Map';
import Feature from 'ol/Feature';
import VectorSource from 'ol/source/Vector';
import Overlay from 'ol/Overlay';
import {Tile as TileLayer, Vector as VectorLayer} from 'ol/layer';
import View from 'ol/View';
import {transform} from 'ol/proj';
import XYZ from 'ol/source/XYZ'
import Point from 'ol/geom/Point';
import GeoJSON from 'ol/format/GeoJSON';
import {Fill, Stroke, Icon, Style} from 'ol/style'
import markerImg from '@/assets/img/markerIcon.png'
export default {
name: "openlayersMap",
data () {
return {
mapObj: null,
mapDom: null,
mapPointList: [],
pointLayerSource:null,
pointLayer: null,
markerIcon: markerImg
}
},
mounted() {
this.initMap()
},
methods: {
// 清除地图 某些情况 地图容器会存在两个 导致地图无法正常显示 这个问题折腾了我半天。
// 找了半天官方貌似也没有提供 对应的 api,自己动手了。
mapClear (){
if (this.mapDom) {
this.mapDom.innerHTML = ''
this.mapDom = null
}
},
// 初始化地图
initMap () {
// 先尝试清除
this.mapClear()
// 获取地图容器
this.mapDom = document.getElementById('map')
// 初始化地图配置
this.mapObj = new Map({
target: this.mapDom, // 地图容器
view: new View({
//设置你的地图初始中心点
center: [117.990969, 36.635013], // 地图中心点
zoom: 10, // 缩放
projection: 'EPSG:4326' // 坐标系
})
})
// 添加一个使用离线瓦片地图的层
const offlineMapLayer = new TileLayer({
source: new XYZ({
//这里的url就是你要改的地方
url: 'http://192.168.3.6:8081' + '/{z}/{x}/{y}.png' // 设置本地离线瓦片所在路径
})
})
// 将图层添加到地图
this.mapObj.addLayer(offlineMapLayer)
// 加载地理坐标
this.addPoint()
},
// 添加地理坐标
addPoint () {
this.delPointAll()
// 地理坐标数组
const pointData = [
{longitude: 117.990969, latitude: 36.635013}
]
pointData.map(item => {
// 创建点
const point = new Feature({
geometry: new Point([item.longitude, item.latitude]),
data: item
})
// 点的样式
const iconStyle = new Style({
image: new Icon({
color: '#ffffff',
crossOrigin: 'anonymous',
src: this.markerIcon,
}),
})
// 设置样式
point.setStyle(iconStyle)
// 保存到数据 方便删除
this.mapPointList.push(point)
})
// 创建geojson据源
this.pointLayerSource = new VectorSource({features: this.mapPointList})
// 创建图层 并加载数据
this.pointLayer = new VectorLayer({source: this.pointLayerSource})
// 将图层添加地图上
this.mapObj.addLayer(this.pointLayer)
},
// 地理点位删除
delPointAll(){
// 判断 删除的数据源是否存在
if (this.pointLayerSource) {
// 遍历删除
this.mapPointList.map(item => {
this.pointLayerSource.removeFeature(item)
})
// 删除图层 重置数据
this.mapObj.removeLayer(this.pointLayer)
this.pointLayerSource = null
this.pointLayer = null
this.mapPointList = []
}
}
},
beforeDestroy() {
this.mapClear()
}
}
</script>
<style scoped>
.map {
width: 100%;
height: 100%;
}
</style>
这里面你需要改的地方基本就那几个
- this.mapObj 中的
center
字段,是用来设置你地图的默认中心点的 - offlineMapLayer中的
url
这个字段,后面的不需要改,只需要给前面的ip+端口
改成你本地启动的就行 - 那么跑起来基本就没啥问题了。
撒点功能的实现
撒点应该很多做地图的都要实现这个功能吧
还是看上述代码块,他中间的addPoint中的
pointData
数组是用来放你要撒的点的坐标的,iconStyle
是用来设置点的样式的,你可以直接给color
删掉,然后给最后的src
赋上图片链接就行了
撒点弹窗功能的实现
这个可以看一下原帖的另一段代码,因为这块我没有用到所以就没去看他
// 地图点击事件
this.mapObj.on('click', function (evt) {
// 获取点击位置的数据
const feature = self.mapObj.forEachFeatureAtPixel(evt.pixel, function (feature) {
return feature;
})
// 根据 点击元素 className 判断是否点击在自定义popup上
const isClickPopUp = evt.originalEvent.path.map(item => item.className).includes('el-card__body')
if (!isClickPopUp) {
popupDom.style.display = 'none'
}
// 官方示例 采用 jq + bootstrap弹窗,但是我觉得没有必要 如果大量使用bootstrap 组件可以考虑引入。
const popupContainer = document.getElementsByClassName('popupContainer')[0]
// 判断数据
if (feature) {
// feature.values_.data ,data字段是在 addPoint 函数创建point时添加 可以自定义
if (feature.values_.data) {
const pointData = feature.values_.data
popup.setPosition(evt.coordinate)
popupContainer.innerHTML = `<div>${pointData.name}</div>`
popupDom.style.display = 'block'
}
}
})
区县范围的实现
这个具体看原帖吧,我忘了当时是咋写的了QAQ
2.截取地图将坐标转换为px
这个方案其实就简单很多了,你需要给地图固定到一个缩放等级,很多东西都是写死的,然后直接通过一套算法去转换就行了
首先data里面要有一些静态的数据
data() {
return {
// 地图缩放等级
zoom: 15,
// 一些默认值
MCBAND: [12890594.86, 8362377.87, 5591021, 3481989.83, 1678043.12, 0],
LLBAND: [75, 60, 45, 30, 15, 0],
MC2LL: [
[
1.410526172116255e-8, 0.00000898305509648872, -1.9939833816331,
200.9824383106796, -187.2403703815547, 91.6087516669843,
-23.38765649603339, 2.57121317296198, -0.03801003308653, 17337981.2,
],
[
-7.435856389565537e-9, 0.000008983055097726239, -0.78625201886289,
96.32687599759846, -1.85204757529826, -59.36935905485877,
47.40033549296737, -16.50741931063887, 2.28786674699375, 10260144.86,
],
[
-3.030883460898826e-8, 0.00000898305509983578, 0.30071316287616,
59.74293618442277, 7.357984074871, -25.38371002664745,
13.45380521110908, -3.29883767235584, 0.32710905363475, 6856817.37,
],
[
-1.981981304930552e-8, 0.000008983055099779535, 0.03278182852591,
40.31678527705744, 0.65659298677277, -4.44255534477492,
0.85341911805263, 0.12923347998204, -0.04625736007561, 4482777.06,
],
[
3.09191371068437e-9, 0.000008983055096812155, 0.00006995724062,
23.10934304144901, -0.00023663490511, -0.6321817810242,
-0.00663494467273, 0.03430082397953, -0.00466043876332, 2555164.4,
],
[
2.890871144776878e-9, 0.000008983055095805407, -3.068298e-8,
7.47137025468032, -0.00000353937994, -0.02145144861037,
-0.00001234426596, 0.00010322952773, -0.00000323890364, 826088.5,
],
],
LL2MC: [
[
-0.0015702102444, 111320.7020616939, 1704480524535203,
-10338987376042340, 26112667856603880, -35149669176653700,
26595700718403920, -10725012454188240, 1800819912950474, 82.5,
],
[
0.0008277824516172526, 111320.7020463578, 647795574.6671607,
-4082003173.641316, 10774905663.51142, -15171875531.51559,
12053065338.62167, -5124939663.577472, 913311935.9512032, 67.5,
],
[
0.00337398766765, 111320.7020202162, 4481351.045890365,
-23393751.19931662, 79682215.47186455, -115964993.2797253,
97236711.15602145, -43661946.33752821, 8477230.501135234, 52.5,
],
[
0.00220636496208, 111320.7020209128, 51751.86112841131,
3796837.749470245, 992013.7397791013, -1221952.21711287,
1340652.697009075, -620943.6990984312, 144416.9293806241, 37.5,
],
[
-0.0003441963504368392, 111320.7020576856, 278.2353980772752,
2485758.690035394, 6070.750963243378, 54821.18345352118,
9540.606633304236, -2710.55326746645, 1405.483844121726, 22.5,
],
[
-0.0003218135878613132, 111320.7020701615, 0.00369383431289,
823725.6402795718, 0.46104986909093, 2351.343141331292,
1.58060784298199, 8.77738589078284, 0.37238884252424, 7.45,
],
],
// 地图可视区域
N: { width: 1226, height: 1926 },
// 当前可视范围中心点坐标
Q: { lng: 121.4391, lat: 31.1676 },
// 坐标点数组
items: [
{ x: 121.445613, y: 31.175118 },
{ x: 121.462861, y: 31.200346 },
{ x: 121.429338, y: 31.145276 },
{ x: 121.411697, y: 31.166757 },
{ x: 121.41999, y: 31.182991 },
],
};
},
下面是一些转换算法
// 定位点转px计算函数
/** *********将地图坐标转换成像素***********/
// point:当前像素 var point = new BMap.Point(x, y);
// zoom:当前地图缩放级别 var zoom = map.getZoom();
// center:当前地图可视范围中心点坐标 var center = map.getCenter();
// bounds:地图可视区域 var bound = map.getSize();
PointToPixel(point, zoom, center, bounds) {
// 坐标到像素
if (!point) {
return;
}
point = this.FormatPoint(point);
center = this.FormatPoint(center);
var units = this.GetZoomUnits(zoom);
var x = Math.round((point.lng - center.lng) / units + bounds.width / 2);
var y = Math.round((center.lat - point.lat) / units + bounds.height / 2);
// 这个应该是转换完成的像素
return { x: x, y: y };
},
// 转换缩放级别
GetZoomUnits(zoom) {
return Math.pow(2, 18 - zoom);
},
FormatPoint(point) {
let lng_lat;
var mc;
point.lng = this.getLoop(point.lng, -180, 180);
point.lat = this.getRange(point.lat, -74, 74);
lng_lat = {
lng: point.lng,
lat: point.lat,
};
for (let i = 0; i < this.LLBAND.length; i++) {
if (lng_lat.lat >= this.LLBAND[i]) {
mc = this.LL2MC[i];
break;
}
}
if (!mc) {
for (let i = this.LLBAND.length - 1; i >= 0; i--) {
if (lng_lat.lat <= -this.LLBAND[i]) {
mc = this.LL2MC[i];
break;
}
}
}
var cE = this.convertor(point, mc);
lng_lat = {
lng: cE.lng.toFixed(2),
lat: cE.lat.toFixed(2),
};
return lng_lat;
},
getLoop(lng, a, b) {
while (lng > b) {
lng -= b - a;
}
while (lng < a) {
lng += b - a;
}
return lng;
},
getRange(lat, a, b) {
if (a != null) {
lat = Math.max(lat, a);
}
if (b != null) {
lat = Math.min(lat, b);
}
return lat;
},
convertor(point, mc) {
if (!point || !mc) {
return;
}
var lng = mc[0] + mc[1] * Math.abs(point.lng);
var c = Math.abs(point.lat) / mc[9];
var lat =
mc[2] +
mc[3] * c +
mc[4] * c * c +
mc[5] * c * c * c +
mc[6] * c * c * c * c +
mc[7] * c * c * c * c * c +
mc[8] * c * c * c * c * c * c;
lng *= point.lng < 0 ? -1 : 1;
lat *= point.lat < 0 ? -1 : 1;
return { lng: lng, lat: lat };
},
isblock(items) {
this.aleat = items;
this.isAleat = !this.isAleat;
},
这些算法你不需要管他都干了什么(我也看不大懂),只需要知道调用PointToPixel方法他会返回给你一个对象,这个对象就是你要的
x
和y
的坐标就行了const px = this.PointToPixel(Q, this.zoom, this.Q, this.N);
只有第一个值是动态传的你的坐标,后面的三个值全部都是死值!
然后你根据这个
px
去做定位就行了,v-for
去循环一堆span
出来然后position: absolute;
动态绑定这个px
就好了,至于点击弹窗的话,你可以给span
外面套个div
然后给span
下面再写个div
,再用定位定上去就行了,至于点击切换也不是很麻烦,然后用伪类写个小箭头就好了!
第二种方案肯定是简单很多的,但是效果肯定是差很多。
这就是我在网上搜了两天找出来的比较合理的地图解决方案,如果觉得有用希望能给点个赞,如果文中有什么错误的地方,希望各位能在评论区指出,我及时的改正!