因离线地图引发的惨案

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

效果如下

相关推荐
u01040583612 分钟前
构建可扩展的Java Web应用架构
java·前端·架构
swimxu1 小时前
npm 淘宝镜像证书过期,错误信息 Could not retrieve https://npm.taobao.org/mirrors/node/latest
前端·npm·node.js
qq_332394201 小时前
pnpm的坑
前端·vue·pnpm
雾岛听风来1 小时前
前端开发 如何高效落地 Design Token
前端
不如吃茶去1 小时前
一文搞懂React Hooks闭包问题
前端·javascript·react.js
冯宝宝^1 小时前
图书管理系统
服务器·数据库·vue.js·spring boot·后端
alwn1 小时前
新知识get,vue3是如何实现在style中使用响应式变量?
前端
来之梦1 小时前
uniapp中 uni.previewImage用法
前端·javascript·uni-app
野猪佩奇0072 小时前
uni-app使用ucharts地图,自定义Tooltip鼠标悬浮显示内容并且根据@getIndex点击事件获取点击的地区下标和地区名
前端·javascript·vue.js·uni-app·echarts·ucharts
2401_857026232 小时前
拖动未来:WebKit 完美融合拖放API的交互艺术
前端·交互·webkit