目录
实现功能点
技术选型
具体实现
Vue3整合高德地图JS API-2.0
添加商户:前端
添加商户:后端/ES
查询用户当前地理坐标
获取附近(指定距离)的商户
总结/测试Demo代码地址
测试概述:用户使用高德地图组件获取商户详细地址和地理坐标(经纬度)存入ES中,然后获取当前用户的地理坐标位置,用户可定义查询半径,用来搜索以用户位置为中心点,指定半径构成搜索圆形区域,搜索出包含在圆形区域内的商户(信息包含排序后的距离)。
实现功能点
-
商户入驻选址:商户使用地图组件标记来选择地址,获取商户选择的地址信息和地理位置传入后端,后端将信息存储到ES中。
-
获取用户当前坐标:浏览器地理定位API:navigator.geolocation,需要用户在弹出授权框中赋予位置权限。
-
显示商户距离:使用ES中的地理查询(圆形过滤)geo_distance类型进行查询附近(指定半径)商户,并且在sort中对于商户远近进行排序。
技术选型
前端:Vite,Vue3.x ,Pinia,ElementPlus
后端:Spring Boot3.x , ElasticSearch7.x
地图相关服务提供:高德地图JS API 2.0
具体实现
Vue3整合高德地图JS API-2.0
1. 安装依赖包:npm i @amap/amap-jsapi-loader ,用于创建地图等相关对象。
2. 配置key和密钥
关于安全问题:一种解决可以将key和密钥放到Vite环境变量中,然后组件内引用。
2.1 注册高德开放平台账号,创建应用获取key和密钥。
访问:lbs.amap.com
注册账号,进入【我的应用】创建服务平台为为Web端的应用。
如果项目需要跑线上,为了保证我们的Key不暴露在代码中,可以将key和密钥信息放到环境变量中,或者使用代理服务器,在代理服务器处配置上密钥。
高德关于key安全说明:准备-入门-教程-地图 JS API 1.4|高德地图API (amap.com)
3. 配置地图参数,初始化显示地图。
<template>
<!-- 高德地图容器 -->
<div ref="mapRef" id="map_container"></div>
</template>
<script setup>
import { load } from '@amap/amap-jsapi-loader';
import { onMounted, ref } from 'vue';
// 对应地图渲染的 ref 元素 <div ref="mapRef" />
const mapRef = ref()
// 配置key和密钥(取自环境变量)
const mapApiKey = reactive({
securityJsCode: import.meta.env.VITE_APP_AMAP_SECURITY_JS_CODE,
key: import.meta.env.VITE_APP_AMAP_API_KEY
})
// 地图相关对象
const map = ref()
let mapO = {};
// 设置地图默认显示的中心点(经度,纬度)
let centerLnglat = ref([109.428071, 24.326442]);
const initMapView = async () => {
try {
// 配置安全密钥
window._AMapSecurityConfig = {
securityJsCode: mapApiKey.securityJsCode
}
// 给map赋予操作构造器
map.value = await load({
key: mapApiKey.key,
version: '2.0', // 指定要加载的 JSAPI 的版本,默认为 1.4.15
plugins: ['AMap.Scale'] //需要使用的的插件列表,多个插件用逗号分隔
})
// 获取地图对象
mapO = new map.value.Map(mapRef.value, {
viewMode: '3D', // 是否为3D地图模式
zoom: 11, // 初始化地图级别
center: centerLnglat.value, // 中心地理坐标
resizeEnable: true
})
// 添加点击事件,创建标记,后续用到
mapO.on('click', OnPoint)
} catch (error) {
console.log(error)
}
}
onMounted(() => {
// 执行地图初始化,显示地图
initMapView()
})
</script>
编写完如上初始化地图代码之后,在地图容器中就能看到地图了。
添加商户:前端
过程:用户点击右上角“加入商户”,显示表单填写商户信息,以及显示高德地图组件,根据用户填写的省市位置重载地图组件中心点,当用户标记地图中指定位置时,获取标记位置的坐标,然后调用【逆地理编码(坐标->地址)】接口将地理坐标转换为详细地址显示。用户可适当修改详细地址后,提交-新增完毕。
根据用户输入字段【省】和【市】重载地图中心点(正向地理编码):
// 地理编码(地址->坐标),如下map.value在上方初始化地图代码中有定义和赋值
function selectLnglat() {
map.value.plugin('AMap.Geocoder', function () {
var geocoder = new map.value.Geocoder({
city: info.address // info.address变量就是省市内容
})
geocoder.getLocation(info.address, function (status, result) {
if (status === 'complete' && result.info === 'OK') {
// result中对应详细地理坐标信息,将地图赋予新地理中心点重载地图
centerLnglat.value[0] = result.geocodes[0].location.lng;
centerLnglat.value[1] = result.geocodes[0].location.lat;
initMapView();
console.log(result);
} else {
console.log(status);
}
})
})
}
标记获取坐标,坐标获取地址(逆地理编码)代码:
<script setup>
// 如上初始化地图代码中声明了:mapO.on('click', OnPoint),给地图添加了点击事件。
// 地图点击处理器,创建标记点
function OnPoint(e) {
// 获取点击位置的经纬度
const { lng, lat } = e.lnglat;
// 处理标记点显示偏移问题
// 如下map.value在上方初始化地图代码中有定义并赋值
const pixel = mapO.lngLatToContainer(e.lnglat); // 获取地图的像素坐标
const offset = new map.value.Pixel(-2, -12);// 图标的偏移量(根据图标的实际大小来设置)
// 计算新的像素坐标
const newPixel = new map.value.Pixel(pixel.getX() + offset.getX(), pixel.getY() + offset.getY());
// 将新的像素坐标转换为经纬度
const newLngLat = mapO.containerToLngLat(newPixel);
// 创建标记点
const marker = new map.value.Marker({
position: newLngLat, // 使用调整后的经纬度位置,标记点图标显示位置
title: '标记点', // 鼠标悬停时显示的文字
icon: 'http://webapi.amap.com/theme/v1.3/markers/n/mark_b.png', // 自定义标记图标 URL(可选)
offset: offset // 偏移量
});
// 将标记点添加到地图上
marker.setMap(mapO);
// 使用经纬度查询出详细地址(逆地理查询)
getDetailAddress(newLngLat.lng, newLngLat.lat);
// 将地理坐标存储,先纬度后经度,因为es存储String类型的地理坐标与WKT相反
// 后续将此坐标存在es中,作为商户的地理坐标位置进行距离搜索
info.setLatlng(newLngLat.lat+","+newLngLat.lng);
// 输出经纬度坐标
console.log("点击经纬度:", lng, lat);
console.log("调整后的经纬度:", newLngLat.lat, newLngLat.lng);
}
</script>
其中有标记点显示时偏移的调整处理,如果在实际开发中发现偏移可适当调整数值。
逆地理编码(根据用户的标记点,推出详细地址) :
// 根据经纬度坐标获取详细地址(逆地理编码(坐标->地址))
function getDetailAddress(lng, lat) {
map.value.plugin('AMap.Geocoder', function () {
var geocoder = new map.value.Geocoder({
city: info.address // 这个值填写:城市名称,或城市代码都可
})
// 构造坐标参数数组
let agrsPotin = [lng, lat];
geocoder.getAddress(agrsPotin, function (status, result) {
if (status === 'complete' && result.info === 'OK') {
// result.regeocode.formattedAddress就是推出的地址
// 将详细地址存入pinia中,在表单组件中取出展示
info.setDetailAddress(result.regeocode.formattedAddress)
} else {
console.log("获取地址失败,", status);
}
})
})
}
添加商户:后端/ES
创建ES的库索引,引入依赖并创建新增接口,接收前端传入的商户信息参数,将数据存储ES中。
1.创建ES的库索引结构。
PUT /hotels
{
"mappings": {
"properties": {
"id":{"type": "keyword"},
"picture": { "type": "keyword" },
"name": { "type": "text","analyzer": "ik_smart"},
"score": { "type": "float"},
"distance": { "type": "float"},
"desc": { "type": "text" },
"newPrice": { "type": "float" },
"oldPrice": { "type": "float" },
"saleMonth": { "type": "integer" },
"detailAddress":{"type": "text"},
"location": {
"type": "geo_point"
}
}
}
}
2.SpringBoot引入ES依赖
推荐引入8以下的,8以上版本Java客户端变化太大,API中文文档不全面。
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.12.1</version>
</dependency>
3.编写对应实体类,和新增接口
// 将ES客户端加入Ioc容器
@Configuration
public class ElasticsearchConfig {
@Value("${esLInfo}") // 连接es信息定义在.yml配置文件中,如http://ip:9200
private String esLInfo;
@Bean
public RestHighLevelClient createEsClient(){
return new RestHighLevelClient(RestClient.builder(HttpHost.create(esLInfo)));
}
}
// 实体类,对应es库索引
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Hotel {
private String id;
private String picture;
private String name;
private Float score;
private String detailAddress;
private String desc;
private Float newPrice;
private Float oldPrice;
private Integer saleMonth;
private String location; // geo_point类型,格式为 "lat,lon"
private String detailAddress;
}
// 新增接口
@PostMapping
public String addHandle(@RequestBody Hotel hotel) throws IOException {
// 构建新增请求
IndexRequest request = new IndexRequest("hotels").id(hotel.getId());
// 准备数据
request.source(gson.toJson(hotel), XContentType.JSON);
IndexResponse index = client.index(request, RequestOptions.DEFAULT);
log.info(index+"===");
return "新增成功";
}
查询用户当前地理坐标
使用浏览器内置函数:navigator.geolocation,实现当前用户地理坐标的查询。
弹出授权框,用户授权后可获取。
<script setup>
// 当前用户的地理坐标
const position = ref(null);
function getUserLocation(){
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(pos) => {
position.value = {
lat: pos.coords.latitude,
lng: pos.coords.longitude,
};
// 根据用户地理坐标获取周围指定距离的酒店
getNearHotel(position.value);
// console.log("当前用户地理位置:",position.value);
},
(err) => {
error.value = err.message;
}
);
} else {
error.value = '浏览器不支持';
}
}
</script>
获取附近(指定距离)的商户
1. 编写后端接口,根据用户当前位置和想要查询的半径获取包含在圆形区域的内商户信息。
// searchVo参数封装定义三个参数:经度,纬度,半径,前端传入值
@PostMapping("/searchByLocation")
public List<Map<String, Object>> searchHotelsByLocation(
@RequestBody SearchVo searchVo
) throws IOException {
// 构建查询请求
SearchRequest searchRequest = new SearchRequest("hotels");
// 构建查询DSL
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
boolQuery.filter(QueryBuilders.geoDistanceQuery("location")
.point(searchVo.getLat(), searchVo.getLon())
.distance(searchVo.getRadiusKm(), DistanceUnit.KILOMETERS));
sourceBuilder.query(boolQuery);
sourceBuilder.sort(SortBuilders.geoDistanceSort("location", searchVo.getLat(), searchVo.getLon())
.order(SortOrder.ASC) // 实现升序排序,距离越近越靠前
.unit(DistanceUnit.KILOMETERS));
searchRequest.source(sourceBuilder);
// 执行查询
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
SearchHits hits = searchResponse.getHits();
List<Map<String, Object>> hotels = new ArrayList<>();
for (SearchHit hit : hits) {
Map<String, Object> hotel = hit.getSourceAsMap();
// 可以获取距离信息(如果需要显示距离)
double distance = (double) hit.getSortValues()[0];
hotel.put("distance_km", distance);
hotels.add(hotel);
}
return hotels;
}
2. 前端传入用户坐标和搜索半径,获取商家数据展示。
// 使用圆形过滤,过滤出当前用户指定距离的附近酒店
async function getNearHotel(userLocation){
// 定义搜索半径,实际可绑定表单组件让用户选择。
let radiusKm = 3.0;
// 构造查询参数Vo
let searchArgs = {
lat:userLocation.lat,
lon:userLocation.lng,
radiusKm:radiusKm
}
const res = await axios.post("http://localhost:9009/es/searchByLocation",searchArgs);
console.log("查询到"+ radiusKm + "公里内,存在"+res.data.length+"家酒店/旅社");
// 处理距离小数点,保留逗号后两位
for(let item of res.data){
item.distance_km = item.distance_km.toFixed(2);
}
// 赋值,显示搜索到的酒店
cardsD.value = res.data
}
总结/测试Demo代码地址
至此,使用高德地图API配合ES完成地理位置查询功能点,测试完毕。
此测试Demo完整代码地址:gitee.com/maohe101/map-es-demo
更多高德地图的JS API可查看文档:概述-地图 JS API 2.0|高德地图API (amap.com)