因离线地图引发的惨案

小王最近接到了一个重要的需求,要求在现有系统上实现离线地图功能,并根据不同的经纬度在地图上显示相应的标记(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代理这个静态服务

js 复制代码
server {
    listen 80;
    server_name yourdomain.com; # 替换为你的域名或服务器 IP

    root /var/www/myapp/public; # 设置根目录
    index index.html; # 设置默认文件

    location / {
        try_files $uri $uri/ =404;
    }

    # 配置访问 roadmap 目录下的地图瓦片
    location /roadmap/ {
        alias /var/www/myapp/public/roadmap/;
        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

js 复制代码
<l-tile-layer url="http://192.168.88.211:8080/{z}/{x}/{y}.png" ></l-tile-layer>
这里的url就是上面启动的服务,包括端口和ip,要能访问到瓦片

编写代码很简单

js 复制代码
<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数据,可以在阿里云数据平台上获取 通过组件加载即可

js 复制代码
<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

编写代码

js 复制代码
<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>

在子组件中构建查询

js 复制代码
<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>

效果如下

相关推荐
To_OC4 小时前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
vipbic5 小时前
中后台越做越乱后,我用插件化把它救回来了
前端·vue.js
Hyyy5 小时前
Computer Use 适合做什么,不适合做什么——一次真实使用后的思考
前端
小和尚同志5 小时前
前端 AI 单元测试思考与落地
前端·人工智能·aigc
invicinble6 小时前
c端系统,其实更像一个信息展示平台
前端
李姆斯7 小时前
管理是否可以被完全量化
前端·产品经理·团队管理
名字还没想好☜8 小时前
Next.js 中间件实战:鉴权、重定向与 A/B 分流
开发语言·前端·javascript·中间件·react·next.js
广州灵眸科技有限公司8 小时前
瑞芯微RV1126B开发板(EASY-EAI-PI2) INI文件操作
java·前端·javascript·网络·人工智能
kaiking_g9 小时前
vue项目中,静态导入 vs 动态导入 详解
前端·javascript·vue.js
江华森9 小时前
Web 开发基础:HTTP 与 REST
前端·网络协议·http