是的,我看过大佬写的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 文件
css
@import "leaflet/dist/leaflet.css";
创建初始化地图组件
在 src/components/InitMap.vue
创建用于初始化地图的组件
ts
<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
文件
ts
// 用于获取图片的路径
export const getAssetsImgFile = (url: string): string => {
return new URL(`../assets/img/${url}`, import.meta.url).href;
};
实现 useGeoUtils
函数
新建 src/composables
目录,创建 useGeoUtils.ts
文件,这里放一些公用的地图相关的处理函数
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
文件,这里实现附近充电宝的相关逻辑
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 组装逻辑
ts
<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
获取区域内点位 - 加载到地图
这里就不展开了,感兴趣可以查看仓库 src/composables/useAreaQueryPoints.ts
函数的实现
总结
这里通过 turfjs 、leaflet
实现了附近充电宝功能,讲解了如何实现根据绘制的多边形,查询多边形内点位的功能 猛击查看代码仓库
实现附近充电宝功能难点是在于如何判断点是否在多边形内
turfjs
提供了 booleanPointInPolygon
方法可以用来判断点是否在多边形接收两个参数 点位坐标、多边形的geoJson
还提供了 circle
接收三个参数 中心点、半径、opts
来生成一个圆的 geoJson 数据,这样就可以将两个函数组合来实现附近充电宝的功能