使用turfjs、地图实现附近充电宝

是的,我看过大佬写的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:  
        '&copy; <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

  1. 监听绘制完成事件,获取多边形的点位
  2. 根据获取到的点位,生成 geoJson
  3. 调用上边 useGeoUtilsgetPointListByGeoJson 获取区域内点位
  4. 加载到地图

这里就不展开了,感兴趣可以查看仓库 src/composables/useAreaQueryPoints.ts 函数的实现

总结

这里通过 turfjs 、leaflet 实现了附近充电宝功能,讲解了如何实现根据绘制的多边形,查询多边形内点位的功能 猛击查看代码仓库

实现附近充电宝功能难点是在于如何判断点是否在多边形内

turfjs 提供了 booleanPointInPolygon 方法可以用来判断点是否在多边形接收两个参数 点位坐标、多边形的geoJson

还提供了 circle 接收三个参数 中心点、半径、opts 来生成一个圆的 geoJson 数据,这样就可以将两个函数组合来实现附近充电宝的功能

往期文章

相关推荐
玩电脑的辣条哥2 小时前
Python如何播放本地音乐并在web页面播放
开发语言·前端·python
ew452182 小时前
ElementUI表格表头自定义添加checkbox,点击选中样式不生效
前端·javascript·elementui
suibian52352 小时前
AI时代:前端开发的职业发展路径拓宽
前端·人工智能
画月的亮2 小时前
element-ui 使用过程中遇到的一些问题及解决方法
javascript·vue.js·ui
Moon.92 小时前
el-table的hasChildren不生效?子级没数据还显示箭头号?树形数据无法展开和收缩
前端·vue.js·html
m0_526119402 小时前
点击el-dialog弹框跳到其他页面浏览器的滚动条消失了多了 el-popup-parent--hidden
javascript·vue.js·elementui
垚垚 Securify 前沿站2 小时前
深入了解 AppScan 工具的使用:筑牢 Web 应用安全防线
运维·前端·网络·安全·web安全·系统安全
工业甲酰苯胺5 小时前
Vue3 基础概念与环境搭建
前端·javascript·vue.js
lyj1689975 小时前
el-tree选中数据重组成树
javascript·vue.js·elementui
mosquito_lover16 小时前
怎么把pyqt界面做的像web一样漂亮
前端·python·pyqt