likes
comments
collection
share

React+高德+Leaflets的简单地图搜索

作者站长头像
站长
· 阅读数 40

1.概述

本项目是一个基于React的地图搜索demo项目,通过使用Leaflets插件作为地图容器,利用高德地图提供的poi接口实现地图搜索,以满足用户在地图上查找特定位置的需求。

源码:Map-search-of-React-Gaud-Leaflets

技术栈:React、Leaflets高德搜索POI 2.0

项目功能:

  1. 关键字搜索
  2. 结果标注到地图上
  3. 鼠标移入结果列表,地图跳转到对应位置并弹窗

例:输入地址搜索后,在地图上标注出结果

React+高德+Leaflets的简单地图搜索

2.地图搜索实现

  • 功能通过高德poi2接口实现,请求api最重要两个字段key关键字,如果想限制只检索某地区则需要adcode
  • 以下只放关键代码,省略了很多配置,具体代码可以看demo源码

2.1输入框

React+高德+Leaflets的简单地图搜索

使用antd的select,如果单纯想要地址搜索,使用普通input组件也行

import { Select, Button } from "antd";
​
const [input, setInput] = useState();
​
<Select showSearch onSearch={setInput}/>
<Button onClick={() => send(input)}></Button>

2.2发送请求

由于跨域问题,所以使用fetch

参数:

  • value:用户输入的关键字
  • key:高德key
  • region:城市adcode(可选)
  • city_limit:是否只在region中配置的的城市中检索(可选)
  • page_size:默认10,最大25(可选)
import React, { useState, useEffect} from "react";
​
const [data, setData] = useState([]);//保存响应
//发送请求
async function send(value) {
    fetch(
        `https://restapi.amap.com/v5/place/text?parameters&key=xxxxxxxxxxxx&keywords=${value}&region=441303&city_limit=true&page_size=25`,
        {
            method: "get",
        }
    )
        .then(async (res) => {
        res = await res.json();
        setData(res.pois);//保存数据
        setShowSearchPopup(true);//弹出下拉菜单
    }
              })
}
​
//响应结果示例
 {
      address: "灿邦新天地首层",
      distance: "",
      pcode: "440000",
      adcode: "441303",
      pname: "广东省",
      cityname: "惠州市",
      type: "餐饮服务;快餐厅;快餐厅",
      typecode: "050300",
      adname: "惠阳区",
      citycode: "0752",
      name: "肯德基(龙山DT店)",
      location: "114.501469,22.755872",
      id: "B0FFKOTLE4",
    },

2.2.1adcode

  • 如果没有在某一城市搜索的需求,可以忽视adcode
  • adcode获取,如果只是想知道本地的adcode,直接在高德开放平台adcode最下方查询即可
  • 如果想限制只在某城市中搜索,除了配置adcode,还需要配置city_limit=true

2.3渲染图标到地图上

当获取到api的响应时,将列表中的点位标注在地图上,地图为leaflet地图实例

2.3.1生成leaflet地图

npm i leaflet  
​
import * as L from "leaflet";
import "leaflet/dist/leaflet.css";

准备地图容器的div

<div id="leaflet" className="map"></div>

生成实例并添加瓦片地图,这里使用mapbox地图服务

const [map, setMap] = useState();
useEffect(() => {
    //生成地图容器实例
    const m = L.map("leaflet");
    //保存实例
    setMap(m);
​
    //添加瓦片地图到地图容器中
    L.tileLayer(
        "https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw",
        {
            id: "mapbox/streets-v11", // map样式
        }
    ).addTo(m);
}, []);

2.3.2添加标点到地图中

这里是核心部分,将api响应的列表添加到地图上

2.3.2.1添加一个标点

利用leaflet的L.marker方法可以将一个标点添加到地图上,但想实现需求还不够

L.marker([51.5, -0.09]).addTo(map);

2.3.2.2添加多个标点

React+高德+Leaflets的简单地图搜索

  • api检索结果中包含了多个标点
  • 由于要添加多个标点,并且要第二次搜索时,肯定要清除第一次标的点
  • 所以需要用一个数组将所有标点的实例保存起来,需要清除时可以统一清除
const [group, setGroup] = useState(null); //保存了所有marker//添加多个标点的方法,arr为api响应列表
const addMarker=(arr)=>{
    const list = arr.map((item, index) => {
        //提取经纬度
        const [longitude, latitude] = item.location?.split(",");
        
        return new L.marker([latitude, longitude], {
            icon: icon(item, index),
        })
    });
​
    //保存图标列表到leaflets图层组
    const group = L.layerGroup(list);
    
    //将图层组统一渲染到地图
    map.addLayer(group);
    
    //保存到state,为清除所有点做准备
    setGroup(group);
}

2.3.2.3给标点绑定弹出框

鼠标悬浮时,弹出提示框。在上方new图标的实例的代码后方添加bindPopup方法即可,想要自定义弹出框可以自己编写样式

React+高德+Leaflets的简单地图搜索

// 弹出框的模板
const popup = (item) =>
`
<div>${item.address}</div>
<div>名称: ${item.name}</div>
<div>类型: ${item.type}</div>
`;
​
//在new图标实例时,绑定弹出框
return new L.marker([latitude, longitude], {
    icon: icon(item, index),
}).bindPopup(popup(item))//绑定弹出框

2.3.2.4useEffect调用添加图标方法

监听res,每当有新的相应,都会调用添加图标方法

问题:不会清空之前生成的图标,图标越来越多

  //监听res,绘制地图标点
  useEffect(() => {
    if (!map || !data || !data.length) return;
​
    addMarker(data);//调用添加图标方法
  }, [data, map]);

2.3.2.5清空之前的图标

调用leaflets的clearLayers()方法,将图标一次性清除

const clearMarker = () => {
    if (group) {
        group.clearLayers();
        setGroup(null);
    }
};

在每次添加图标之前调用清空方法

//监听res,绘制地图标点
useEffect(() => {
if (!map || !data || !data.length) return;
clearMarker()//调用清空方法
addMarker(data);//调用添加图标方法
}, [data, map]);

2.4搜索结果列表

列表将名称和地址拼接到一起,并且左边名称深色,右边地址浅色,利用JSX实现

React+高德+Leaflets的简单地图搜索

2.4.1使用antd的selct下拉菜单

实现左右拼接的效果,需要使用jsx

<Select
    showSearch
    onSearch={setInput}
    //列表格式化之后,传给Select组件
    options={formatt(data)}
    />
//格式化之后的对象:
//{
// value:id,
// label: <div className="seachPopupText" >
//           <span className="left">地名</span>&nbsp;
//           <span className="right">地址</span>
//         </div>
//}

//格式化输入列表,返回label是地名和地址拼接的jsx
  const formatt = (list) => {
    return list?.map((item) => {
      return {
        value:item.id,//id
        label: (//将名称和地址拼接到一起
          <div
            className="seachPopupText"
          >
            <span className="left">{item.name}</span>&nbsp;
            <span className="right">{item.address}</span>
          </div>
        ),
        ...item,
      };
    });
  };
/* 搜索框下拉菜单的文本css */
.seachPopupText .left {
  color: rgb(0, 0, 0);
  font-weight: 700;
}
.seachPopupText .right {
  color: rgba(64, 64, 64, 0.831);
}

2.4.2鼠标移入列表时弹出

React+高德+Leaflets的简单地图搜索

实现方法:

  • 在列表labeldiv绑定onMouseEnter鼠标移入事件
  • 触发事件时,生成一个popup添加到地图上
  • autoPanPadding:关键配置,当弹出框弹出时,移动地图使弹出框在比较中间的位置
return list?.map((item) => {
    return {
        value:item.id,
        label: (
            <div
                className="seachPopupText"
                //绑定鼠标移入事件
                onMouseEnter={() => {
                    textPopup(item);//弹出popup
                }}
                >
                <span className="left">{item.name}</span>&nbsp;
                <span className="right">{item.address}</span>
            </div>
        ),
    };
});
​
//侧边栏鼠标经过时,弹出悬浮框
const textPopup = (item) => {
    const [longitude, latitude] = coordtransform.gcj02towgs84(
        ...item.location?.split(",")
    );
​
    const p = L.popup({
        //移动地图使弹出框在比较中间的位置
        autoPanPadding: L.point(400, 150),
        keepInView: false, //在边界弹出时,不会被边界遮挡
        offset: L.point(0, -16), //往上偏移,不遮挡标点
    })
    .setLatLng({ lat: latitude, lng: longitude })
    .setContent(popup(item))
    .openOn(map);
}

3.其他细节

3.1坐标纠偏

返回的经纬度是gcj02坐标系

渲染到wgs84坐标系地图时会有偏移,需要纠偏

可以使用coordtransform插件

npm i coordtransform
​
import coordtransform from 'coordtransform';
​
const [longitude, latitude] = coordtransform.gcj02towgs84(...item.location?.split(","));

3.2控制下拉菜单弹出

React+高德+Leaflets的简单地图搜索

3.2.1通过按钮来控制下拉菜单

通过给select配置open字段来实现open={showSearchPopup}

const [showSearchPopup, setShowSearchPopup] = useState(false);
​
<Select
    showSearch
    onSearch={setInput}
    options={formatt(data)}
    //主动控制是否弹出
    open={showSearchPopup}
    //获得焦点时也弹出
    onFocus={()=>{
    if (data?.length) setShowSearchPopup(true);
  }}
    />
//有数据时才显示按钮
{data?.length !== 0 &&
        (showSearchPopup ? (
         //收起
          <Button
            shape="circle"
            onClick={() => setShowSearchPopup(false)}
            icon={<UpOutlined />}
          ></Button>
        ) : (
          //弹出
          <Button
            shape="circle"
            onClick={() => setShowSearchPopup(true)}
            icon={<DownOutlined />}
          ></Button>
        ))}

3.3useRef主动失去焦点

React+高德+Leaflets的简单地图搜索

  • 在选中之后,输入框没有失去焦点
  • antd有提供取消焦点方法,在选中列表之后主动调用失去焦点方法
  • 使用uesRef调用

import  { useRef} from "react";
​
const selectRef = useRef();//获取组件
​
<Select ref={selectRef} />
​
selectRef.current.blur();//失去焦点

3.4下拉菜单宽度调整

React+高德+Leaflets的简单地图搜索

如果想让下拉菜单对齐左右的按钮,直接改父级div宽度是不行的

因为父级div是绝对定位是antd控制的,所以通过相对定位改变子div的位置,并且改子div的宽度

.searchPopup {
  overflow: visible;
  background-color: transparent;
}
.searchPopup > div {
  position: relative;
  right: 34px;
  width: 376px;
  background-color: #fff;
}

修改之后:

React+高德+Leaflets的简单地图搜索