openlayers 实战离线地图

本文 从零到一 带你实战 openlayers 离线地图的使用,内容包括:

  • 获取离线地图瓦片
  • openlayers加载离线地图瓦片
  • 显示地理坐标
  • 显示地理坐标范围。
  • openlayers 官网 这个是一定要看的。openlayers的api 相比于 商用的地图(比如 高德 百度 之类的)确实不怎么友好。

获取离线地图瓦片

  • 高德是提供了一个api 感兴趣的可以去看看,本文不采用那种方式。
  • 下载离线地图瓦片,网上有很多方法、大部分收费 找个合适的工具真的难,找到一个能用的免费贡献出来了全能地图下载器
  • 链接:pan.baidu.com/s/18LiUAh1-… 提取码:yd88

image.png

image.png

  • 离线瓦片下载完成后的到这样一份数据

image.png

发布离线数据

  • 地图离线瓦片肯定是要放在服务器上的,放在项目里有点过于庞大了。
  • 本地测试的话,本地起一个服务用来访问 地图离线瓦片,本文采用 http-server 猛击查看安装 一行命令。

image.png

openlayers 加载离线地图 本文以 vue 项目为例

安装 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: '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>
复制代码

地理点位追加

  • 图层数据源 VectorSource 创建后提供一个 addFeature 方法 pointLayerSource.addFeature(item) item 是创建 point 点数据如:
// 创建点
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)
复制代码

不出意外页面会展示地图 和一个定位点 如果没有先进行如下排查

  • 上边的服务第一次请求 会卡住 放到cmd 敲几下回车 即可(有请求产生)
  • 查看 离线地图瓦片 请求路径是否正确

image.png

  • 坐标是否正确,是否在所展示地图辖区内。

点位响应事件 展示 popup弹窗

<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: 'http://192.168.3.6:8081' + '/{z}/{x}/{y}.png'  // 设置本地离线瓦片所在路径
        })
      })
      // 将图层添加到地图
      this.mapObj.addLayer(offlineMapLayer)

      // 地图点击事件
      this.mapOnClick()
      // 加载地理坐标
      this.addPoint()
    },

    // 地图点击事件
    mapOnClick (){
      const self = this

      // popupDom
      const popupDom = document.getElementById('popup')
      // 创建 popup
      const popup = new Overlay({
        element: popupDom,
        positioning: 'bottom-center',
        stopEvent: false
      })
      // 加载到地图
      this.mapObj.addOverlay(popup)

      // 地图点击事件
      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'
          }
        }
      })
    },

    // 添加地理坐标
    addPoint () {
      this.delPointAll()
      // 地理坐标数组
      const pointData = [
        {longitude: 117.990969, latitude: 36.635013, name: '李大壮'}
      ]

      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>
复制代码

image.png

添加行政区域范围

  • 行政区域 是指 省 市 区的范围,一般市一份geojson 数据,可以从这里获取 datav geo

image.png

  • 注意图层的顺序 点位图层不在最上层的话 可能会导致 点击事件无法正确响应 图层先添加的在下边
<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 {inject, onMounted, watch, onUnmounted} from 'vue'
import home from "@model/home";

export default {
  name: "MapBox",
  props: {
    pointData: {
      type: Array,
      default: []
    }
  },
  emits: ['mapClickPoint'],
  setup(props, {emit}) {
    const {APP_MAP_ZOOM, APP_MAP_TILES_URL} = window.appConfig


    let mapObj = null
    let mapDom = null

    const mapClear = () => {
      if (mapObj) {
        mapDom.innerHTML = ''
      }
    }
    onUnmounted(() => {
      mapClear()
    })
    const initMap = () => {
      mapClear()
      mapDom = document.getElementById('map')
      // 地图设置中心,设置到成都,在本地离线地图 offlineMapTiles刚好有一张zoom为4的成都瓦片
      const center = transform([117.990969, 36.635013], 'EPSG:4326', 'EPSG:3857');

      mapObj = new Map({
        target: mapDom,
        view: new View({
          center: center,
          zoom: APP_MAP_ZOOM,
          projection: 'EPSG:3857'
        })
      })

      // 添加一个使用离线瓦片地图的层
      const offlineMapLayer = new TileLayer({
        source: new XYZ({
          url: APP_MAP_TILES_URL + '/{z}/{x}/{y}.png'  // 设置本地离线瓦片所在路径
        })
      });

      mapObj.addLayer(offlineMapLayer)
      mapOnClick()
      addAreaPolygon()
      getGpsList()

      watch(() => props.pointData, () => {
        addPoint()
      }, {immediate: true, deep: true})
    }

    // 地图点击事件
    const mapOnClick = () => {
      const popupDom = document.getElementById('popup')
      const popup = new Overlay({
        element: popupDom,
        positioning: 'bottom-center',
        stopEvent: false
      })
      mapObj.addOverlay(popup)

      mapObj.on('click', function (evt) {
        const feature = 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) {
          if ( feature.values_.gpsPointData) {
            const pointData = feature.values_.gpsPointData
            popup.setPosition(evt.coordinate)
            popupContainer.innerHTML = `<div>${pointData.name}</div>`
            popupDom.style.display = 'block'
          }

          emit('mapClickPoint', pointData)
        } else {
          emit('mapClickPoint', {})
        }
      })
    }

    let mapPointList = []
    let pointLayerSource = null
    let pointLayer = null
    const addPoint = () => {
      delPointAll()

      props.pointData.map(item => {
        if (item.checked) {
          item.poi_list.map(pointItem => {
            pointItem.panelType = item.value
            const point = new Feature({
              geometry: new Point(transform([pointItem.longitude, pointItem.latitude], 'EPSG:4326', 'EPSG:3857')),
              data: pointItem
            })

            const imgSrc = require(`@/assets/img/map/icon_${item.value}.png`)
            const iconStyle = new Style({
              image: new Icon({
                color: '#ffffff',
                crossOrigin: 'anonymous',
                src: imgSrc,
              }),
            })
            point.setStyle(iconStyle)

            mapPointList.push(point)
          })
        }
      })

      // 数据  图层
      pointLayerSource = new VectorSource({features: mapPointList})
      pointLayer = new VectorLayer({source: pointLayerSource})
      mapObj.addLayer(pointLayer)
    }
    const delPointAll = () => {
      if (pointLayerSource) {
        mapPointList.map(item => {
          pointLayerSource.removeFeature(item)
        })

        mapObj.removeLayer(pointLayer)
        pointLayerSource = null
        pointLayer = null
        mapPointList = []
      }
    }

    // 添加区县范围
    const addAreaPolygon = () => {
      let geoJson = require('@/mock/zb.json')
      const vectorSource = new VectorSource({
        features: new GeoJSON({featureProjection: 'EPSG:3857'}).readFeatures(geoJson),
      })

      const layer = new VectorLayer({
        source: vectorSource,
        style: new Style({
          stroke: new Stroke({
            color: 'blue',
            lineDash: [4],
            width: 3,
          }),
          fill: new Fill({
            color: 'rgba(0, 0, 0, 0.1)',
          }),
        }),
      });
      mapObj.addLayer(layer)
    }


    // todo 获取ws 推送数据 通过 provide 于 home文件注入
    const wsDataInfo = inject('wsDataInfo')
    watch(wsDataInfo, () => {
      if (wsDataInfo.value) {
        let data = JSON.parse(wsDataInfo.value.data)
        const type = data.pushType + ''

        console.log(data, 'p[p[[')
        if (type === '2') {
          addGpsPoint([data], false)
        }
      }
    }, {immediate: true, deep: true})

    // 获取gps 点位列表
    const getGpsList = () => {
      home.getGpsList().then(res => {
        if (res.state === 0) {
          // addGpsPoint(res.resultInfo)
        }
      })
    }
    let gpsPointList = []
    let gpsPointLayerSource = null
    let gpsPointLayer = null
    const addGpsPoint = (data, del = true) => {

      // 最初是加载多个点。。。。
      let pointList = []
      data.map(item => {
        const point = new Feature({
          geometry: new Point(transform([item.longitude, item.latitude], 'EPSG:4326', 'EPSG:3857')),
          gpsPointData: item
        })

        const imgSrc = require(`@/assets/img/map/gpsIcon.png`)
        const iconStyle = new Style({
          image: new Icon({
            color: '#ffffff',
            crossOrigin: 'anonymous',
            src: imgSrc,
          }),
        })
        point.setStyle(iconStyle)

        pointList.push(point)
      })

      // 数据  图层
      gpsPointList.push(...pointList)


      if (!gpsPointLayerSource) {
        // 数据  图层
        gpsPointLayerSource = new VectorSource({features: gpsPointList})
        gpsPointLayer = new VectorLayer({source: gpsPointLayerSource})
        mapObj.addLayer(gpsPointLayer)
      } else {
        pointList.map(item => {
          gpsPointLayerSource.addFeature(item)
        })
      }
    }
    const delGpsPointAll = () => {
      if (gpsPointLayerSource) {
        gpsPointList.map(item => {
          gpsPointLayer.removeFeature(item)
        })

        mapObj.removeLayer(gpsPointLayer)
        gpsPointLayerSource = null
        gpsPointLayer = null
        gpsPointList = []
      }
    }

    onMounted(() => {
      initMap()
    })
  }
}
</script>

<style lang="scss" scoped>
.map {
  width: 100%;
  height: 100%;
}
</style>
复制代码

image.png

也算是新手上路 边看文档编写,如果错误 或更好的写法 还请小伙伴们指出,谢谢!

猜你喜欢

转载自juejin.im/post/7017301189406490655