开发一个前后端分离的webgis城市共享单车投放管理系统(2)

255 阅读3分钟

文章同步更新于我的个人博客:松果猿的博客,欢迎访问获取更多技术分享。

同时,您也可以关注我的微信公众号:松果猿的代码工坊,获取最新文章推送和编程技巧。

前言

上一期我们将这个系统的前端部分的大致框架构建好了,现在我们继续后续的开发任务

加载南京geojson文件

加载本地geojson文件,在asset下添加/data/nanjing.json,这里推荐一个网站进行地图格式转换:mapshaper.org/。通过如下方法进行加载:

导入json文件:

image-20241118145535624

添加一个矢量图层用于加载南京geojson文件

image-20241118173808334

这样南京矢量边界就添加上去了

image-20241118173908886

实现绘制点线面功能

地图菜单栏和地图怎么实现通信呢,我们用pinia

新建@/stores/mapStore.js,用于绘制操作的状态管理

@/stores/mapStore.js

import { defineStore } from 'pinia';

export const useMapStore = defineStore('map', {
  state: () => ({
    currentDrawType: null,
    vectorSource: null,
  }),
  actions: {
    setDrawType(type) {
      this.currentDrawType = type;
    },
    setVectorSource(source) {
      this.vectorSource = source;
    },
    clearDraw() {
      if (this.vectorSource) {
        this.vectorSource.clear();
      }
    }
  },
}); 

currentDrawType用于存储当前绘制类型(点线面),vectorSource用于存储当前绘制矢量图层源,用于操作后续的清除绘制图层操作,setDrawType(type) 用于设置当前绘制类型,setVectorSource用于设置当前矢量数据源,clearDraw()用于进行矢量图层的清除操作

新建@/components/DrawButton.vue

<template>
  <div class="draw-button">
    <el-button type="primary" @click="stopDraw">结束绘制</el-button>
    <el-button type="warning" @click="clearDraw">清空绘制</el-button>
  </div>
</template>

<script setup>
import { useMapStore } from "@/stores/mapStore";
const mapStore = useMapStore();

const stopDraw = () => {
  mapStore.setDrawType(null);
};

const clearDraw = () => {
  mapStore.clearDraw();
};
</script>

<style scoped>
.draw-button{
  display: flex;
  justify-content: center;
  align-items: center;
  position: absolute;
  top: 20px;
  left: 50%;
  transform: translateX(-50%);
  gap: 10px;
}
</style>

这个组件用于进行绘制的停止清除功能

@/components/MapControl.vue中添加如下代码将绘制类型存储在pinia中

image-20241118210652504

image-20241118210908546

再来到components/OlMap.vue

添加绘制图层的矢量源和图层:

const vectorSource = ref(new VectorSource());
const Drawvector = new VectorLayer({
  source: vectorSource.value,
  style: new Style({
    fill: new Fill({
      color: "rgba(255, 255, 255, 0.5)",
    }),
    stroke: new Stroke({
      color: "#ffcc33",
      width: 2,
    }),
    image: new Circle({
      radius: 7,
      fill: new Fill({
        color: "#ffcc33",
      }),
    }),
  }),
});

在挂载钩子中添加

onMounted(() => {
  initMap();
  // 设置矢量数据源
  mapStore.setVectorSource(vectorSource.value);
});

添加绘制交互


const addDrawInteraction = (drawType) => {
  if (draw.value) {
    map.value.removeInteraction(draw.value);
  }

  let geometryType;
  switch (drawType) {
    case "drawPoint":
      geometryType = "Point";
      break;
    case "drawLine":
      geometryType = "LineString";
      break;
    case "drawPolygon":
      geometryType = "Polygon";
      break;
    default:
      return;
  }

  
  draw.value = new Draw({
    source: vectorSource.value,
    type: geometryType,
  });

  map.value.addInteraction(draw.value);
};

监听pinia中数据的变化,当绘制交互改变时

watch(
  () => mapStore.currentDrawType,
  (newType) => {
      addDrawInteraction(newType);
    }
);

把绘制操作组件引入主页页面

image-20241118212058441

实现弹窗以及高亮显示

我在上一期说过openlayer内置有业务图层对象Overlayers,这个图层可以展示弹框

新建@/components/PopupMenu.vue

<template>
  <div id="popup" class="ol-popup">
    <a
      href="#"
      id="popup-closer"
      class="ol-popup-closer"
      @click="closePopup"
    ></a>
    <div id="popup-content"></div>
  </div>
</template>

<script setup>

</script>

<style scoped>
/*弹出框样式*/
.ol-popup {
  position: absolute;
  bottom: 12px;
  left: -50px;
  background-color: white;
  box-shadow: rgb(247, 247, 241);
  padding: 15px;
  border-radius: 10px;
  border: 1px solid #cccccc;
  min-width: 280px;
}

.ol-popup:after {
  position: absolute;
  top: 100%;
  height: 0px;
  width: 0px;
  left: 48px;
  content: "";
  margin-left: -11px;
  border-width: 11px;
  border: 1px solid transparent;
}

.ol-popup:after {
  border-top-color: white;
}

.ol-popup-closer {
  text-decoration: none;
  position: absolute;
  top: 2px;
  right: 8px;
}

.ol-popup-closer:after {
  content: "⨉";
}
</style>

引入:

image-20241118214046861

设置响应类型和高亮样式:

const popup = ref(null);

const highlightStyle = new Style({
  stroke: new Stroke({
    color: "red",
    width: 3,
  }),
  fill: new Fill({
    color: "rgba(255,0,0,0.1)",
  }),
});

添加监听事件


const addPopup = () => {
  const container = document.getElementById("popup");
  const content = document.getElementById("popup-content");
  const closer = document.getElementById("popup-closer");

  closer.onclick = () => {
    popup.value.setPosition(undefined);
  };

  popup.value = new Overlay({
    element: container,
    autoPan: true,
    autoPanAnimation: {
      duration: 250,
    },
  });

  map.value.addOverlay(popup.value);

  let highlightedFeature = null;

  map.value.on("click", (evt) => {
    const feature = map.value.forEachFeatureAtPixel(
      evt.pixel,
      (feature, layer) => {
        // 检查是否是南京图层
        if (layer === NanjingLayer.value && !draw.value) {
          return feature;
        }
      }
    );

    // 重置之前高亮的要素样式
    if (highlightedFeature) {
      highlightedFeature.setStyle(undefined);
      highlightedFeature = null;
    }

    if (feature) {
      const coordinates = evt.coordinate;
      feature.setStyle(highlightStyle);
      highlightedFeature = feature;

      content.innerHTML = `
        <h3>南京市</h3>
        <p>这里是南京市,江苏省会,简称"宁"。</p>
        <p>面积:6587平方公里</p>
        <p>人口:约850万</p>
      `;
      popup.value.setPosition(coordinates);
    } else {
      popup.value.setPosition(undefined);
    }
  });
};

注意要把之前的南京矢量图层设置为响应类型,因为高亮显示在监听点击事件时检验是否为南京图层:layer === NanjingLayer.value && !draw.value引用了该图层

image-20241118220137872

ok,完成了

高亮显示:

image-20241118215824777

绘制操作:

20241118_220415

项目地址:github.com/songguo1/Sh…