是的,我看过大佬写的Nest + Redis + 地图,实现附近的充电宝,这给我提供了写作的灵感,感谢大佬!
既然通过后端支持可以实现附近点位检索的功能,那么依靠纯前端可以实现吗?
答案是可以的,唯一的难点在于如何判断点是否在多边形内?

这个我不会写但是我知道有个库有方法可以直接用,来一起往下看。
需求本次实现两个小需求,都比较简单
附近充电宝: 每次移动地图后获取地图中心点,展示中心点附近指定范围的充电宝。
区域查询:在地图上绘制一个多边形(圆、四方形、三角形也可以算是多边形),根据绘制的多边形查询区域内的点位并在地图上展示。
这两个需求很类似都可以看作是查询指定范围内的点位,大概如下图所(xia)示(hua)
实现附近充电宝需求
简单初始化一个 vite、vue 项目这里就不展开讲了,社区已经有很多了。
安装依赖执行 pnpm i leaflet @turf/turf 来安装依赖,如果是 ts 项目还需要安装 pnpm i @types/leaflet -D。
leaflet 是一个开源的地图引擎,有着丰富的插件。
turfjs是一个开源的空间分析库,可用于判断点是否在多边形内。
初始化地图使用 leaflet + 天地图底图 来实现地图功能
在 src/style.css 中引入 leaflet css 文件
@import "leaflet/dist/leaflet.css";
创建初始化地图组件
在 src/components/InitMap.vue 创建用于初始化地图的组件
<script setup lang="ts"> import { onMounted, onUnmounted, ref } from 'vue'; import L from 'leaflet'; defineProps({ width: { type: String, default: '100%' }, height: { type: String, default: '100%' } }); const emit = defineEmits(['mapLoad']); const mapRef = ref(); const initMap = () => { const map = L.map(mapRef.value, { center: [39.92, 116.4], zoom: 14, minZoom: 0, maxZoom: 20 }); const mapType = 'vec'; L.tileLayer( 'https://t{s}.tianditu.gov.cn/' + mapType + '_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=' + mapType + '&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=b72aa81ac2b3cae941d1eb213499e15e', { subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'], attribution: '© <a href="http://lbs.tianditu.gov.cn/home.html">天地图 GS(2022)3124号 - 甲测资字1100471</a>' } ).addTo(map); const mapLabelType = 'cva'; L.tileLayer( 'https://t{s}.tianditu.gov.cn/' + mapLabelType + '_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=' + mapLabelType + '&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILECOL={x}&TILEROW={y}&TILEMATRIX={z}&tk=b72aa81ac2b3cae941d1eb213499e15e', { subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'] } ).addTo(map); // 地图初始化完成发送事件 emit('mapLoad', map); return map; }; const mapObj = ref(); // 在 onMounted 中初始化地图 onMounted(() => { mapObj.value = initMap(); }); const removeMap = () => { if (mapObj.value) { mapObj.value.remove(); } }; // 在组件卸载时删除地图 onUnmounted(() => { removeMap(); }); </script> <template> <div ref="mapRef" class="map"></div> </template> <style scoped> .map { width: v-bind(width); height: v-bind(height); z-index: 0; } </style>
造一些数据用于测试
之前写过一篇生成指定辖区内随机点的文章,使用这个网站来获取一些随机点
以北京市为例测试数据有个 1w+ 就够用了存成 json 压缩完大概 500k,记得把文件放到 public 目录下 public/pointList.json
需求实现下载一个充电宝的 icon 放到 src/assets/img/mapIcon.png 目录下后边在地图上展示点位会用到
创建 src/utils/tool.ts 文件// 用于获取图片的路径export const getAssetsImgFile = (url: string): string => { return new URL(`../assets/img/${url}`, import.meta.url).href; };
实现 useGeoUtils 函数
新建 src/composables 目录,创建 useGeoUtils.ts 文件,这里放一些公用的地图相关的处理函数
import pointList from '../../public/pointList.json'; // @ts-ignore 库的类型导出似乎不正确 import { booleanPointInPolygon, point as turfPoint } from '@turf/turf'; import L from 'leaflet'; import { getAssetsImgFile } from '@/utils/tool.ts'; export const useGeoUtils = () => { // 根据 geoJson 过滤出数据 const getPointListByGeoJson = (geoJson: any): number[][] => { const list = JSON.parse(JSON.stringify(pointList.list)); // booleanPointInPolygon 用于判断点是否在多边形内 // item.reverse 是因为 pointList 存储的数据和 turfjs 需要的格式是反的 return list.filter((item: number[]) => booleanPointInPolygon(turfPoint(item.reverse()), geoJson) ); }; // 创建 marker const createMarker = (point: number[]) => { const icon = L.icon({ iconUrl: getAssetsImgFile('mapIcon.png'), iconSize: [40, 40] }); return L.marker(point as L.LatLngExpression, { icon }); }; return { getPointListByGeoJson, createMarker }; };
实现 useQueryNearbyPoints 函数
在 src/composables 目录下,创建 useQueryNearbyPoints.ts 文件,这里实现附近充电宝的相关逻辑
import { ref } from 'vue'; import type { Ref } from 'vue'; import { useGeoUtils } from './useGeoUtils.ts'; // @ts-ignore import { circle as turfCircle } from '@turf/turf'; import L from 'leaflet'; export const useQueryNearbyPoints = (mapObj: Ref) => { const circleOverlay = ref(); // 清除绘制的圆形覆盖物 const clearCircleOverlay = () => { if (circleOverlay.value) { circleOverlay.value.remove(); circleOverlay.value = null; } }; / 获取一个圆的 geoJson 并加载这个圆到地图上 @param center 圆的中心点 @param radius 圆的半径 @param units 半径单位 miles, kilometers, degrees, or radians / const getCircleGeoJson = ( center: number[], radius: number, units = 'kilometers' ) => { // 每次绘制前先清除上次的绘制,保证地图上只有一个圆 clearCircleOverlay(); // steps 越大越圆, 圆是由三角形组成的 const options = { steps: 128, units, properties: {} }; const circleGeoJson = turfCircle(center, radius, options); // 获取 geoJson 的同时同步创建一个覆盖物 circleOverlay.value = L.geoJSON(circleGeoJson, { style: { color: '#3875F6' } }).addTo(mapObj.value); return circleGeoJson; }; const { getPointListByGeoJson, createMarker } = useGeoUtils(); // marker 图层组 const markerLayerGroup = ref(); // 清除 marker 图层组 const clearMarkerLayerGroup = () => { if (markerLayerGroup.value) { mapObj.value.removeLayer(markerLayerGroup.value); markerLayerGroup.value = null; } }; // 查询附近点位并加载到地图上 const queryNearbyPoints = (point: number[], radius = 2) => { clearMarkerLayerGroup(); // 获取根据中心点生成的圆 geoJson const circleGeoJson = getCircleGeoJson(point, radius); // 根据 geoJson 查询范围点位 const list = getPointListByGeoJson(circleGeoJson); // 生成 marker 点位 const markerList = list.map((item) => createMarker(item.reverse())); // 将点位加载到地图 markerLayerGroup.value = L.layerGroup(markerList).addTo(mapObj.value); }; // 清除全部 const clearAllNearbyPoints = () => { clearCircleOverlay(); clearMarkerLayerGroup(); }; return { queryNearbyPoints, clearAllNearbyPoints }; };
在 App.vue 组装逻辑
<script setup lang="ts"> import { defineAsyncComponent, ref } from 'vue'; import { useQueryNearbyPoints } from '@/composables/useQueryNearbyPoints.ts'; const InitMap = defineAsyncComponent(() => import('@/components/InitMap.vue')); const mapObj = ref(); const { queryNearbyPoints, clearAllNearbyPoints } = useQueryNearbyPoints(mapObj); const moveQueryNearbyPoints = () => { // 获取移动后的地图中心点 const center = mapObj.value.getCenter(); // 根据中心点查询指定半径内的点位 queryNearbyPoints([center.lng, center.lat]); }; const mapLoad = (map: any) => { mapObj.value = map; // 地图初始化的时候查询一次数据 moveQueryNearbyPoints(); // 监听地图移动结束事件,获取中心点查询数据 mapObj.value.on('moveend', moveQueryNearbyPoints); }; </script> <template> <div class="map-box"> <init-map class="map" @mapLoad="mapLoad"></init-map> </div> </template> <style scoped> .map-box { position: relative; width: 100vw; height: 100vh; .map { position: absolute; top: 0; left: 0; } } </style>
整体比较简单有思路就很好写
实现区域查询这个需求和上边需求类似,是使用插件在地图画一个多边形然后查询多边形内的点位,简单写下实现步骤吧
绘制插件使用 leaflet-geoman-free
监听绘制完成事件,获取多边形的点位根据获取到的点位,生成 geoJson调用上边 useGeoUtils 中 getPointListByGeoJson 获取区域内点位加载到地图总结这里通过 turfjs 、leaflet 实现了附近充电宝功能,讲解了如何实现根据绘制的多边形,查询多边形内点位的功能。
实现附近充电宝功能难点是在于如何判断点是否在多边形内。
turfjs 提供了 booleanPointInPolygon 方法可以用来判断点是否在多边形接收两个参数 点位坐标、多边形的geoJson。
还提供了 circle 接收三个参数 中心点、半径、opts来生成一个圆的 geoJson 数据,这样就可以将两个函数组合来实现附近充电宝的功能。
原文链接:https://juejin.cn/post/7295644677742985226