使用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 数据,这样就可以将两个函数组合来实现附近充电宝的功能

往期文章

相关推荐
m0_7482309424 分钟前
Rust赋能前端: 纯血前端将 Table 导出 Excel
前端·rust·excel
qq_5895681032 分钟前
Echarts的高级使用,动画,交互api
前端·javascript·echarts
黑客老陈2 小时前
新手小白如何挖掘cnvd通用漏洞之存储xss漏洞(利用xss钓鱼)
运维·服务器·前端·网络·安全·web3·xss
正小安2 小时前
Vite系列课程 | 11. Vite 配置文件中 CSS 配置(Modules 模块化篇)
前端·vite
编程百晓君2 小时前
一文解释清楚OpenHarmony面向全场景的分布式操作系统
vue.js
暴富的Tdy2 小时前
【CryptoJS库AES加密】
前端·javascript·vue.js
neeef_se2 小时前
Vue中使用a标签下载静态资源文件(比如excel、pdf等),纯前端操作
前端·vue.js·excel
m0_748235612 小时前
web 渗透学习指南——初学者防入狱篇
前端