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

往期文章

相关推荐
黄尚圈圈22 分钟前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水1 小时前
简洁之道 - React Hook Form
前端
正小安3 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch5 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光5 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   5 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   5 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web5 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常5 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇6 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器