因离线地图引发的惨案
小王最近接到了一个重要的需求,要求在现有系统上实现离线地图功能,并根据不同的经纬度在地图上显示相应的标记(marks)。老板承诺,如果小王能够顺利完成这个任务,他的年终奖就有着落了。
为了不辜负老板的期望并确保自己能够拿到年终奖,小王开始了马不停蹄的开发工作。他查阅了大量文档,研究了各种离线地图解决方案,并一一尝试。经过48小时的连续奋战,凭借着顽强的毅力和专业的技术能力,小王终于成功完成了需求。
他在系统中集成了离线地图,并实现了根据经纬度显示不同区域标记的功能。每个标记都能准确地反映地理位置的信息,系统的用户体验得到了极大的提升。小王的心中充满了成就感和对未来奖励的期待。
然而,天有不测风云。当小王准备向老板汇报工作成果时,却得知一个令人震惊的消息:老板因涉嫌某些违法行为(爬取不当得利)被逮捕了,公司也陷入了一片混乱。年终奖的承诺随之泡汤,甚至连公司未来的发展都蒙上了一层阴影。
尽管如此,小王并没有因此而气馁。这次通过技术让老板成功的获得了编制,他深知只有不断技术的积累和经验的增长才能更好的保护老板。
1.离线地图
首先需要怎么做呢,你需要一个地图瓦片生成器(爬取谷歌、高德、百度等各个平台的地图瓦片,其实就是一张张缩小的图片,这里爬取可以用各种技术手段,但是违法偶,老板就是这么进去的),有个工具推荐:
链接:pan.baidu.com/s/1nflY8-KL…
提取码:yqey
下载解压打开下面的文件
打开了界面就长这样
可以调整瓦片样式
下载速度龟慢,建议开启代理,因为瓦片等级越高数量越多,需要下载的包越大,这里建议下载到11-16级别,根据自己需求
下载完瓦片会保存在自己定义的文件夹,这里不建议放在c盘,会生成以下文件
使用一个文件服务去启动瓦片额静态服务,可以使用http-server
安装http-server
yarn add http-server -g
cd到下载的mapabc目录下
http-server roadmap
本地可以这么做上线后需要使用nginx代理这个静态服务
server {
listen 80;
server_name yourdomain.com; # 替换为你的域名或服务器 IP
root /var/www/myapp/public; # 设置根目录
index index.html; # 设置默认文件
location / {
try_files $uri $uri/ =404;
}
# 配置访问 roadmap 目录下的地图瓦片
location ^~/roloadmap/{
alias /home/d5000/iot/web/roloadmap/;
autoindex on; # 如果你想列出目录内容,可以开启这个选项
}
# 配置其他静态文件的访问(可选)
location /static/ {
alias /var/www/myapp/public/static/;
}
# 其他配置,例如反向代理到应用服务器等
# location /api/ {
# proxy_pass http://localhost:3000;
# proxy_set_header Host $host;
# proxy_set_header X-Real-IP $remote_addr;
# proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# proxy_set_header X-Forwarded-Proto $scheme;
# }
}
配置完重启一下ngix即可
对于如何将瓦片结合成一张地图并在vue2中使用,这里采用vueLeaflet,它是在leaflet基础上进行封装的
这个插件需要安装一系列包
yarn add leaflet vue2-leaflet leaflet.markercluster
<l-tile-layer url="http://192.168.88.211:8080/{z}/{x}/{y}.png" ></l-tile-layer>
这里的url就是上面启动的服务,包括端口和ip,要能访问到瓦片
编写代码很简单
<template>
<div class="map">
<div class="search">
<map-search @input_val="inputVal" @select_val="selectVal" />
</div>
<div class="map_container">
<l-map
:zoom="zoom"
:center="center"
:max-bounds="bounds"
:min-zoom="9"
:max-zoom="15"
:key="`${center[0]}-${center[1]}-${zoom}`"
style="height: 100vh; width: 100%"
>
<l-tile-layer
url="http://192.168.88.211:8080/{z}/{x}/{y}.png"
></l-tile-layer>
<l-marker-cluster>
<l-marker
v-for="(marker, index) in markers"
:key="index"
:lat-lng="marker.latlng"
:icon="customIcon"
@click="handleMarkerClick(marker)"
>
<l-tooltip :offset="tooltipOffset">
<div class="popup-content">
<p>设备名称: {{ marker.regionName }}</p>
<p>主线设备数量: {{ marker.endNum }}</p>
<p>边缘设备数量: {{ marker.edgNum }}</p>
</div>
</l-tooltip>
</l-marker>
</l-marker-cluster>
</l-map>
</div>
</div>
</template>
<script>
import { LMap, LTileLayer, LMarker, LPopup, LTooltip, LMarkerCluster } from "vue2-leaflet";
import mapSearch from "./search.vue";
import "leaflet/dist/leaflet.css";
import L from "leaflet";
// import geojsonData from "./city.json"; // 确保这个路径是正确的
import geoRegionData from "./equip.json"; // 确保这个路径是正确的
// 移除默认的图标路径
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: require("leaflet/dist/images/marker-icon-2x.png"),
iconUrl: require("leaflet/dist/images/marker-icon.png"),
shadowUrl: require("leaflet/dist/images/marker-shadow.png"),
});
export default {
name: "Map",
components: {
LMap,
LTileLayer,
LMarker,
LPopup,
LTooltip,
mapSearch,
LMarkerCluster
},
data() {
return {
zoom: 9,
center: [32.0617, 118.7636], // 江苏省的中心坐标
bounds: [
[30.7, 116.3],
[35.1, 122.3],
], // 江苏省的地理边界
markers: geoRegionData,
customIcon: L.icon({
iconUrl: require("./equip.png"), // 自定义图标的路径
iconSize: [21, 27], // 图标大小
iconAnchor: [12, 41], // 图标锚点
popupAnchor: [1, -34], // 弹出框相对于图标的锚点
shadowSize: [41, 41], // 阴影大小(如果有)
shadowAnchor: [12, 41], // 阴影锚点(如果有)
}),
tooltipOffset: L.point(10, 10), // 调整偏移值
};
},
methods: {
inputVal(val) {
// 处理输入值变化
this.center = val;
this.zoom = 15;
},
selectVal(val) {
// 处理选择值变化
this.center = val;
this.zoom = 15;
},
handleMarkerClick(marker) {
this.center = marker.latlng;
this.zoom = 15;
},
},
};
</script>
<style scoped lang="less">
@import "~leaflet/dist/leaflet.css";
@import "~leaflet.markercluster/dist/MarkerCluster.css";
@import "~leaflet.markercluster/dist/MarkerCluster.Default.css";
.map {
width: 100%;
height: 100%;
position: relative;
.search {
position: absolute;
z-index: 1000;
left: 20px;
top: 10px;
padding: 10px; /* 设置内边距 */
}
}
.popup-content {
font-family: Arial, sans-serif;
text-align: left;
}
.popup-content h3 {
margin: 0;
font-size: 16px;
font-weight: bold;
}
.popup-content p {
margin: 4px 0;
font-size: 14px;
}
/deep/.leaflet-control {
display: none !important; /* 隐藏默认控件 */
}
/deep/.leaflet-control-zoom {
display: none !important; /* 隐藏默认控件 */
}
</style>
这里使用遇到一个坑,需要切换地图中心center,需要给l-map绑定一个key="${center[0]}-${center[1]}-${zoom}
",不然每次切换第一次会失败,第二次才能成功
可以给行政区添加范围,这里需要geojson数据,可以在阿里云数据平台上获取
通过组件加载即可
<l-geo-json :geojson="geojson"></l-geo-json>
效果如下
以上方法,不建议使用,如果是商业使用,不建议使用,不然容易被告侵权,最好能是使用官方合法的地图api,例如谷歌、百度、腾讯、高德,这里我使用高德api给兄弟们们展示一下
2.高德在线地图
2.1首先需要在高德的开放平台申请一个账号
创建一个项目,如下,我们需要使用到这个key和密钥,这里如果是公司使用可以使用公司的信息注册一个账号,公司的账号权限高于个人,具体区别如下参看官网
developer.amap.com/api/faq/acc…
2.2如何在框架中使用
因为不想在创建一个react应用了,这里还是用vue2演示,vue2需要下载一个高德提供的npm包
yarn add @amap/amap-jsapi-loader
编写代码
<template>
<div class="map">
<div class="serach">
<map-search @share_id="shareId" @input_val="inputVal" @select_val="selectVal" @change_theme="changeTheme" />
</div>
<div class="map_container" id="container"></div>
</div>
</template>
<script>
import AMapLoader from "@amap/amap-jsapi-loader";
import mapSearch from "./search.vue";
import cityJson from "../../assets/area.json";
window._AMapSecurityConfig = {
//这里是高德开放平台创建项目时生成的密钥
securityJsCode: "xxxx",
};
export default {
name: "mapContainer",
components: { mapSearch },
mixins: [],
props: {},
data() {
return {
map: null,
autoOptions: {
input: "",
},
auto: null,
AMap: null,
placeSearch: null,
searchPlaceInput: "",
polygons: [],
positions: [],
//地图样式配置
inintMapStyleConfig: {
//设置地图容器id
viewMode: "3D", //是否为3D地图模式
zoom: 15, //初始化地图级别
rotateEnable: true, //是否开启地图旋转交互 鼠标右键 + 鼠标画圈移动 或 键盘Ctrl + 鼠标左键画圈移动
pitchEnable: true, //是否开启地图倾斜交互 鼠标右键 + 鼠标上下移动或键盘Ctrl + 鼠标左键上下移动
mapStyle: "amap://styles/whitesmoke", //设置地图的显示样式
center: [118.796877, 32.060255], //初始化地图中心点位置
},
//地图配置
mapConfig: {
key: "xxxxx", // 申请好的Web端开发者Key,首次调用 load 时必填
version: "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
plugins: [
"AMap.AutoComplete",
"AMap.PlaceSearch",
"AMap.Geocoder",
"AMap.DistrictSearch",
], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
},
// 实例化DistrictSearch配置
districtSearchOpt: {
subdistrict: 1, //获取边界不需要返回下级行政区
extensions: "all", //返回行政区边界坐标组等具体信息
},
//这里是mark中的设置
icon: {
type: "image",
image: require("../../assets/equip.png"),
size: [15, 21],
anchor: "bottom-center",
fitZoom: [14, 20], // Adjust the fitZoom range for scaling
scaleFactor: 2, // Zoom scale factor
maxScale: 2, // Maximum scale
minScale: 1 // Minimum scale
}
};
},
created() {
this.initMap();
},
methods: {
//初始化地图
async initMap() {
this.AMap = await AMapLoader.load(this.mapConfig);
this.map = new AMap.Map("container", this.inintMapStyleConfig);
//根据地理位置查询经纬度
this.positions = await Promise.all(cityJson.map(async item => {
try {
const dot = await this.queryGeocodes(item.cityName, this.AMap);
return {
...item,
dot: dot
};
} catch (error) {
}
}));
//poi查询
this.addMarker();
//显示安徽省的区域
this.drawBounds("安徽省");
},
//查询地理位置
async queryGeocodes(newValue, AMap) {
return new Promise((resolve, reject) => {
//加载行政区划插件
const geocoder = new AMap.Geocoder({
// 指定返回详细地址信息,默认值为true
extensions: 'all'
});
// 使用地址进行地理编码
geocoder.getLocation(newValue, (status, result) => {
if (status === 'complete' && result.geocodes.length) {
const geocode = result.geocodes[0];
const latitude = geocode.location.lat;
const longitude = geocode.location.lng;
resolve([longitude, latitude]);
} else {
reject('无法获取该地址的经纬度');
}
});
});
},
//结合输入提示进行POI搜索
shareId(val) {
this.autoOptions.input = val;
},
//根据设备搜索
inputVal(val) {
if (val?.length === 0) {
//poi查询
this.addMarker();
//显示安徽省
this.drawBounds("安徽省");
return;
}
var position = val
this.icon.size = [12, 18]
this.map.setCenter(position)
this.queryPoI()
this.map.setZoom(12, true, 1);
},
//修改主题
changeTheme(val) {
const styleName = "amap://styles/" + val;
this.map.setMapStyle(styleName);
},
//区域搜索
selectVal(val) {
if (val && val.length > 0) {
let vals = val[val?.length - 1];
vals = vals.replace(/\s+/g, '');
this.queryPoI()
this.placeSearch.search(vals);
this.drawBounds(vals);
this.map.setZoom(15, true, 1);
}
},
//添加marker
addMarker() {
const icon = this.icon
let layer = new this.AMap.LabelsLayer({
zooms: [3, 20],
zIndex: 1000,
collision: false,
});
// 将图层添加到地图
this.map.add(layer);
// 普通点
let markers = [];
this.positions.forEach((item) => {
const content = `
<div class="custom-info-window">
<div class="info-window-header"><b>${item.cityName}</b></div>
<div class="info-window-body">
<div>边设备数 : ${item.edgNum} 台</div>
<div>端设备数 : ${item.endNum} 台</div>
</div>
</div>
`;
let labelMarker = new AMap.LabelMarker({
position: item.dot,
icon: icon,
rank: 1, //避让优先级
});
const infoWindow = new AMap.InfoWindow({
content: content, //传入字符串拼接的 DOM 元素
anchor: "top-left",
});
labelMarker.on('mouseover', () => {
infoWindow.open(this.map, item.dot);
});
labelMarker.on('mouseout', () => {
infoWindow.close();
});
labelMarker.on('click', () => {
this.map.setCenter(item.dot)
this.queryPoI()
this.map.setZoom(15, true, 1);
})
markers.push(labelMarker);
});
// 一次性将海量点添加到图层
layer.add(markers);
},
//POI查询
queryPoI() {
this.auto = new this.AMap.AutoComplete(this.autoOptions);
this.placeSearch = new this.AMap.PlaceSearch({
map: this.map,
}); //构造地点查询类
this.auto.on("select", this.select);
this.addMarker();
},
//选择数据
select(e) {
this.placeSearch.setCity(e.poi.adcode);
this.placeSearch.search(e.poi.name); //关键字查询查询
this.map.setZoom(15, true, 1);
},
// 行政区边界绘制
drawBounds(newValue) {
//加载行政区划插件
if (!this.district) {
this.map.plugin(["AMap.DistrictSearch"], () => {
this.district = new AMap.DistrictSearch(this.districtSearchOpt);
});
}
//行政区查询
this.district.search(newValue, (_status, result) => {
if (Object.keys(result).length === 0) {
this.$message.warning("未查询到该地区数据");
return
}
if (this.polygons != null) {
this.map.remove(this.polygons); //清除上次结果
this.polygons = [];
}
//绘制行政区划
result?.districtList[0]?.boundaries?.length > 0 &&
result.districtList[0].boundaries.forEach((item) => {
let polygon = new AMap.Polygon({
strokeWeight: 1,
path: item,
fillOpacity: 0.1,
fillColor: "#22886f",
strokeColor: "#22886f",
});
this.polygons.push(polygon);
});
this.map.add(this.polygons);
this.map.setFitView(this.polygons); //视口自适应
});
},
},
};
</script>
<style lang="less" scoped>
.map {
width: 100%;
height: 100%;
position: relative;
.map_container {
width: 100%;
height: 100%;
}
.serach {
position: absolute;
z-index: 33;
left: 20px;
top: 10px;
}
}
</style>
<style>
//去除高德的logo
.amap-logo {
right: 0 !important;
left: auto !important;
display: none !important;
}
.amap-copyright {
right: 70px !important;
left: auto !important;
opacity: 0 !important;
}
/* 自定义 infoWindow 样式 */
.custom-info-window {
font-family: Arial, sans-serif;
padding: 10px;
border-radius: 8px;
background-color: #ffffff;
max-width: 250px;
}
</style>
在子组件中构建查询
<template>
<div class="box">
<div class="input_area">
<el-input placeholder="请输入设备名称" :id="search_id" v-model="input" size="mini" class="input_item" />
<img src="../../assets/input.png" alt="" class="img_logo" />
<span class="el-icon-search search" @click="searchMap"></span>
</div>
<div class="select_area">
<el-cascader :options="options" size="mini" placeholder="选择地域查询" :show-all-levels="false" :props="cityProps"
clearable v-model="cityVal" @change="selectCity"></el-cascader>
</div>
<div class="date_area">
<el-select v-model="themeValue" placeholder="请选择地图主题" size="mini" @change="changeTheme">
<el-option v-for="item in themeOptions" :key="item.value" :label="item.label" :value="item.value">
</el-option>
</el-select>
</div>
</div>
</template>
<script>
import cityRegionData from "../../assets/area"
import cityJson from "../../assets/city.json";
export default {
name: "search",
components: {},
mixins: [],
props: {},
data() {
return {
search_id: "searchId",
input: "",
options: cityRegionData,
cityProps: {
children: "children",
label: "business_name",
value: "business_name",
checkStrictly: true
},
cityVal: "",
themeOptions: [
{ label: "标准", value: "normal" },
{ label: "幻影黑", value: "dark" },
{ label: "月光银", value: "light" },
{ label: "远山黛", value: "whitesmoke" },
{ label: "草色青", value: "fresh" },
{ label: "雅士灰", value: "grey" },
{ label: "涂鸦", value: "graffiti" },
{ label: "马卡龙", value: "macaron" },
{ label: "靛青蓝", value: "blue" },
{ label: "极夜蓝", value: "darkblue" },
{ label: "酱籽", value: "wine" },
],
themeValue: ""
};
},
computed: {},
watch: {},
mounted() {
this.sendId();
},
methods: {
sendId() {
this.$emit("share_id", this.search_id);
},
searchMap() {
console.log(this.input,'ssss');
if (!this.input) {
this.$emit("input_val", []);
return
}
let val = cityJson.find(item => item.equipName === this.input)
if (val) {
this.$emit("input_val", val.dot);
return
}
this.$message.warning("未查询到该设备,请输入正确的设备名称");
},
selectCity() {
this.$emit("select_val", this.cityVal);
},
changeTheme(val) {
this.$emit("change_theme", val);
}
},
};
</script>
<style lang="less" scoped>
.box {
display: flex;
.input_area {
position: relative;
width: 170px;
height: 50px;
display: flex;
align-items: center;
.input_item {
width: 100%;
/deep/ .el-input__inner {
padding-left: 30px !important;
}
}
.img_logo {
position: absolute;
left: 5px;
top: 50%;
transform: translateY(-50%);
width: 20px;
height: 20px;
margin-right: 10px;
}
span {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 16px;
color: #ccc;
cursor: pointer;
}
}
.select_area {
width: 150px;
display: flex;
align-items: center;
height: 50px;
margin-left: 10px;
}
.date_area {
width: 150px;
display: flex;
align-items: center;
height: 50px;
margin-left: 10px;
}
}
</style>
效果如下
来源:juejin.cn/post/7386650134744596532