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

往期文章

相关推荐
并不会28 分钟前
常见 CSS 选择器用法
前端·css·学习·html·前端开发·css选择器
衣乌安、32 分钟前
【CSS】居中样式
前端·css·css3
兔老大的胡萝卜32 分钟前
ppk谈JavaScript,悟透JavaScript,精通CSS高级Web,JavaScript DOM编程艺术,高性能JavaScript pdf
前端·javascript
低代码布道师34 分钟前
CSS的三个重点
前端·css
耶啵奶膘2 小时前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^4 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie4 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic5 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿5 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具5 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端