React+高德+Leaflets的简单地图搜索
1.概述
本项目是一个基于React的地图搜索demo项目,通过使用Leaflets插件作为地图容器,利用高德地图提供的poi接口实现地图搜索,以满足用户在地图上查找特定位置的需求。
源码:Map-search-of-React-Gaud-Leaflets
技术栈:React、Leaflets、高德搜索POI 2.0
项目功能:
- 关键字搜索
- 结果标注到地图上
- 鼠标移入结果列表,地图跳转到对应位置并弹窗
例:输入地址搜索后,在地图上标注出结果
2.地图搜索实现
- 功能通过高德poi2接口实现,请求api最重要两个字段key和关键字,如果想限制只检索某地区则需要adcode
- 以下只放关键代码,省略了很多配置,具体代码可以看demo源码
2.1输入框
使用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}®ion=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添加多个标点
- 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
方法即可,想要自定义弹出框可以自己编写样式
// 弹出框的模板
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实现
2.4.1使用antd的selct下拉菜单
实现左右拼接的效果,需要使用jsx
<Select
showSearch
onSearch={setInput}
//列表格式化之后,传给Select组件
options={formatt(data)}
/>
//格式化之后的对象:
//{
// value:id,
// label: <div className="seachPopupText" >
// <span className="left">地名</span>
// <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>
<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鼠标移入列表时弹出
实现方法:
- 在列表
label
的div
绑定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>
<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控制下拉菜单弹出
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主动失去焦点
- 在选中之后,输入框没有失去焦点
- antd有提供取消焦点方法,在选中列表之后主动调用失去焦点方法
- 使用uesRef调用
import { useRef} from "react";
const selectRef = useRef();//获取组件
<Select ref={selectRef} />
selectRef.current.blur();//失去焦点
3.4下拉菜单宽度调整
如果想让下拉菜单对齐左右的按钮,直接改父级div宽度是不行的
因为父级div是绝对定位是antd控制的,所以通过相对定位改变子div的位置,并且改子div的宽度
.searchPopup {
overflow: visible;
background-color: transparent;
}
.searchPopup > div {
position: relative;
right: 34px;
width: 376px;
background-color: #fff;
}
修改之后:
转载自:https://juejin.cn/post/7208858443630624828