前言: 最近在开发类似于迪士尼度假区地图,和智慧景区的小程序地图。 目标发布到小程序上访问。
网上查阅了需要多资料。完成后自己简单整理的笔记,方便有需要的小伙伴可以借鉴思路。
高德地图key 申请传送门:https://console.amap.com/dev/key/app
高德地图Api :https://lbs.amap.com/api/javascript-api/reference/core
高德地图事例:https://lbs.amap.com/demo/list/jsapi-v2
//utils/index.js 拉取所依赖的第三方地图 需要把个人key给换上。export default function MapLoader () { return new Promise((resolve, reject) => { if (window.AMap) { resolve(window.AMap); } else { var script = document.createElement('script'); script.type = "text/javascript"; script.async = true; script.src = "https://webapi.amap.com/maps?v=2.0&key=apikey&callback=initAMap&plugin=AMap.MarkerCluster,AMap.Scale,AMap.HawkEye,AMap.ToolBar,AMap.ControlBar,AMap.MouseTool"; script.onerror = reject; document.head.appendChild(script); } window.initAMap = () => { resolve(window.AMap); }; })}
引入地图资源之后,准备开始处理覆盖图层。 高德提供了以下几种方式。
自有数据图参数,点此查看官方文档
HeatMap
VectorLayer
LabelsLayer
Customlaver
Flexible
lmageLayer
CanvasLayer
GLcustomLayer
最后我选择了下面两种比较贴合的方案。
图片图层 new AMap.ImageLayer()
Flexible瓦片切片 new AMap.TileLayer.Flexible()
使用imageLayer 加点聚合, 点击索引放大的,选中该项交互。
<template> <div class="warp"> <div id="map"></div> <div class="info" id="text">请用鼠标在地图上操作试试</div> <div class="list" v-if="!showCurrentMarker" :class="[{ showListMaxHeight: showListMaxHeight }]" > <div class="tab-container" @touchstart="touchstart" @touchmove="touchmove" > <div class="tab-item" v-for="(item, index) in tabList" :key="index" :class="[{ active: tabIndex == index }]" @click="onChangeTab(item, index)" > <div class="item-icon"> <img :src="item.iconImg" :alt="item.name" /> </div> <div class="item-name">{{ item.name }}</div> </div> </div> </div> <transition name="fade" mode="in-out" appear> <div class="markerCard" v-if="showCurrentMarker"> <div class="marker-card-info"> <div class="left"> <div class="avator"> <img :src="currentMarker.avator" width="60" height="60" alt="" /> </div> </div> <div class="right"> <p class="des">{{ currentMarker.description }}</p> <p class="time">{{ currentMarker.date }}</p> </div> </div> <div class="like"> <span class="left"> 收藏 <img @click="isLike = !isLike" v-if="isLike" src="./static/images/like.svg" class="like-icon" width="20" height="20" alt="" /> <img @click="isLike = !isLike" v-if="!isLike" src="./static/images/d_like.svg" class="like-icon" width="20" height="20" alt="" /> </span> <span class="right">{{ currentMarker.name }}</span> </div> </div> </transition> </div></template><script>import AMap from "./utils/index"; import dwy from "./assets/double-zip-map.png"; import spring from "./assets/poster-drop-animate-bg.png"; import HotelImg from "./static/images/common_img/baobiao.png"; import facilityImg from "./static/images/common_img/baosun.png"; import activeImg from "./static/images/common_img/dingdan.png"; import Mock from "mockjs"; export default { data() { return { /** 地图 参数 */ zoom: 15, map: null, resMap: null, bgLayer: null, //背景图层 HotelMarker: [], // initLat: 19.956588, //初始维度 initLng: 110.12331, //初始经度 latRank: [19.935116, 19.967664], lngRank: [110.106321, 110.162529], /**list tab参数 */ markerType: "all", currentMarker: {}, showCurrentMarker: false, isLike: false, tabList: [ { iconImg: require("./static/images/common_img/baobiao.png"), name: "全部", type: "all", }, { iconImg: require("./static/images/common_img/baosun.png"), name: "公园", type: "facility", }, { iconImg: require("./static/images/common_img/dingdan.png"), name: "酒店", type: "active", }, { iconImg: require("./static/images/common_img/ruku.png"), name: "娱乐", type: "facility", }, { iconImg: require("./static/images/common_img/baobiao.png"), name: "影城", type: "hotel", }, ], tabIndex: 0, showListMaxHeight: false, startX: null, startY: null, moveX: null, moveY: null, }; }, mounted() { this.initAMap(); }, computed: {}, methods: { onChangeTab(item, index) { this.tabIndex = index; this.$nextTick(() => { const container = this.$el.querySelector(".tab-container"); const activeTab = this.$el.querySelector(".tab-item.active"); container.scrollLeft = activeTab.offsetLeft - (container.offsetWidth - activeTab.offsetWidth) / 2; }); this.markerType = item.type; this.markerCluster(); this.map.setZoomAndCenter(13, [this.initLng, this.initLat]); }, touchstart(e) { // 如果你要阻止点击事件,请反注释下一行代码 // e.preventDefault() this.startX = e.touches[0].clientX; this.startY = e.touches[0].clientY; }, touchmove(e) { // e.preventDefault() this.moveX = e.touches[0].clientX; this.moveY = e.touches[0].clientY; if (this.startY - this.moveY <= 0) { console.log("你在往下滑"); this.showListMaxHeight = false; } else { console.log("你在往上滑"); this.showListMaxHeight = true; } }, // 创建地图 和 图层 async initAMap() { try { this.resMap = await AMap(); this.$nextTick(() => { // 创建 Canvas 对象 var imageLayer = new this.resMap.ImageLayer({ // 这是你 地图铺在哪里的地方 bounds: new this.resMap.Bounds( // 第一位 +的话右移 - 左移 [110.094183, 19.928589], //图片左下角 [110.170962, 19.979357] // 图片右上角 ), url: dwy, // 这是你覆盖在地图的图片 zIndex: 3, zooms: [12, 20], // 设置可见级别,[最小级别,最大级别] }); var map = new this.resMap.Map("map", { center: [this.initLng, this.initLat], zoom: this.zoom, zooms: [13, 18], baseRender: "d", //强制使用栅格图 layers: [this.resMap.createDefaultLayer()], showLabel: false, }); this.map = map; // 设置图层 map.add(imageLayer); // console.log(this.resMap.getImageUrl, 'this.map.getImageUrl()'); // 设置背景图层 this.addLayer(); // 添加地图点击事件 this.map.on("click", this.showInfoClick); //限制地图可视范围 this.lockMapBounds(); // 点聚合集群 this.markerCluster(); }); } catch (e) { console.log(e); } }, // 设置背景图层 addLayer() { var layer = new this.resMap.TileLayer.Flexible({ cacheSize: 30, opacity: 1, zIndex: 1, createTile: function (x, y, z, success, fail) { // if ((x + y) % 3) { // fail(); // return; // } var img = document.createElement("img"); img.onload = function () { success(img); }; img.crossOrigin = "anonymous"; // 必须添加,同时图片要有跨域头 img.onerror = function () { fail(); }; img.src = spring; }, }); this.bgLayer = layer; this.map.addLayer(this.bgLayer); }, showInfoClick(e) { console.log(e); var zoom = this.map.getZoom(); //获取当前地图级别 var text = "您在 [ " + e.lnglat.getLng() + "," + e.lnglat.getLat() + " ] 的位置单击了地图!当前层级" + zoom; document.querySelector("#text").innerText = text; this.showCurrentMarker = false; }, // 限制地图范围 lockMapBounds() { // var bounds = this.map.getBounds(); var mybounds = new this.resMap.Bounds( [110.179265, 19.990099], [110.089335, 19.920219] ); this.map.setLimitBounds(mybounds); }, // 创建地图marker createdMarker() { var markers = []; var positions = [ [114.198361, 23.580953], [114.192361, 23.581953], [114.194361, 23.582953], [114.196361, 23.583953], [114.197361, 23.584953], ]; for (var i = 0, marker; i < positions.length; i++) { marker = new this.resMap.Marker({ map: this.map, position: positions[i], icon: "//a.amap.com/jsapi_demos/static/demo-center/icons/poi-marker-default.png", offset: new this.resMap.Pixel(-13, -30), }); } console.log(this.markers); markers.push(marker); }, // 创建点聚合集群 markerCluster() { // 数据中需包含经纬度信息字段 lnglat var points = [ { lnglat: [114.198361, 23.580953], type: "hotel", }, { lnglat: [114.198361, 23.580953], type: "hotel", }, { lnglat: [114.198361, 23.580953], type: "hotel", }, { lnglat: [114.198361, 23.580953], type: "hotel", }, { lnglat: [114.168361, 23.590953], type: "facility" }, { lnglat: [114.168361, 23.590953], type: "facility" }, { lnglat: [114.168361, 23.590953], type: "facility" }, { lnglat: [114.168361, 23.590953], type: "facility" }, { lnglat: [114.170361, 23.580953], type: "active" }, ]; let result = []; points.forEach((item) => { const position = this.setMapMathFloor(); if (item.type === this.markerType || this.markerType === "all") { result.push({ lnglat: position, type: item.type, title: `当前marker,位于经维${position}`, }); } }); console.log(result, "result++="); this.addCluster(result); }, addCluster(points) { if (this.cluster) { this.cluster.setMap(null); } var _renderMarker = (context) => { const type = context && context?.data[0].type; const image = type === "hotel" ? HotelImg : type === "facility" ? facilityImg : activeImg; var Icon = new this.resMap.Icon({ // 图标尺寸 size: new this.resMap.Size(32, 32), // 图标的取图地址 image: image, // 图标所用图片大小 imageSize: new this.resMap.Size(32, 32), }); context.marker.setIcon(Icon); }; console.log(points, "points++++++"); this.cluster = new this.resMap.MarkerCluster(this.map, points, { gridSize: 80, // 设置网格像素大小 zIndex: 10, renderMarker: _renderMarker, // 自定义非聚合点样式 }); /** **v2.0中的点击聚合点散开功能需自己写 **/ this.cluster.on("click", (item) => { if (item.clusterData.length <= 1) { console.log(item); const cluster = item.clusterData[0]; // 移动到具体图标 this.map.setZoomAndCenter(16.5, [ cluster.lnglat.lng, cluster.lnglat.lat, ]); const data = Mock.mock({ username: "@cname()", //随机生成中文名字 date: "@date()", description: "@cword(10)", //描述 name: "@cword(4)", avator: "@image('200x200','#50B347','#fff','avatar')", //生成图片,参数:size,background,foreground,text }); this.currentMarker = { ...data, lng: cluster.lnglat.lng, lat: cluster.lnglat.lat, }; this.showCurrentMarker = true; this.isLike = false; console.log(data); return; } else { let alllng = 0, alllat = 0; for (const mo of item.clusterData) { (alllng += mo.lnglat.lng), (alllat += mo.lnglat.lat); } const lat = alllat / item.clusterData.length; const lng = alllng / item.clusterData.length; this.map.setZoomAndCenter(this.map.getZoom() + 2, [lng, lat]); // if (this.map.getZoom() < 4) { // this.map.setZoomAndCenter(this.map.getZoom() + 8, [lng, lat]); // } else if (this.map.getZoom() < 8) { // this.map.setZoomAndCenter(this.map.getZoom() + 4, [lng, lat]); // } else { // this.map.setZoomAndCenter(this.map.getZoom() + 2, [lng, lat]); // } } }); }, // 清除所有点 removeCluster() { this.cluster.setMap(null); }, setMapMathFloor() { var lng = this.randomNum(this.lngRank[0], this.lngRank[1], 6); var lat = this.randomNum(this.latRank[0], this.latRank[1], 6); console.log(lng, lat, "++++++++"); return [lng, lat]; }, randomNum(maxNum, minNum, decimalNum) { // 获取指定范围内的随机数, decimalNum指小数保留多少位 var max = 0, min = 0; minNum <= maxNum ? ((min = minNum), (max = maxNum)) : ((min = maxNum), (max = minNum)); return (Math.random() * (max - min) + min).toFixed(decimalNum); }, // 地图平移方法 mapPanTo(position) { if (!position && position.length !== 2) { return; } this.map.panTo(position); }, }, };</script><style lang="scss" scoped>.warp { width: 100%; height: 100%; background: url("./static/images/common_img/bg@2x.png") no-repeat center center; background-size: 100% 100%; position: relative; overflow: hidden; .list { background: white; border-radius: 24px; height: calc(100% - 60px); width: 100%; position: absolute; top: calc(100% - 106px); left: 0; z-index: 2; // transform: translateY(calc(100% - 106px)); transition: all 0.25s linear; box-shadow: 7.6px 5.8px 22.2px rgba(0, 0, 0, 0.109), 61px 46px 177px rgba(0, 0, 0, 0.19); &::before { width: 30px; height: 3px; content: ""; position: absolute; background: #ccc; border: 50%; overflow: hidden; top: 6px; left: 50%; transform: translateX(-50%); } .tab-container { display: flex; margin-top: 10px; overflow-x: auto; transition: all 0.25s linear; border-bottom: 1px solid #f4f5f6; .tab-item { display: flex; flex-direction: column; align-items: center; padding: 20px; width: auto; cursor: pointer; .item-icon { img { width: 36px; height: 36px; } } .item-name { margin-top: 2px; word-break: keep-all; } opacity: 0.4; &.active { opacity: 1; } } } } .list.showListMaxHeight { // transform: translateY(0); // bottom: 0; top: 106px; } #map { width: 100%; height: 100%; ::v-deep .amap-logo { left: 2000%; } ::v-deep .amap-copyright { left: 2000%; } } #text { position: absolute; top: 0; right: 0; padding: 20rpx; background: white; z-index: 100; font-size: 24rpx; } .markerCard { transition: all 0.25s linear; position: absolute; bottom: 0; right: 0; z-index: 3; height: 150px; width: 100%; background: white; font-size: 24rpx; border-radius: 24px 24px 0 0; box-shadow: 7.6px 5.8px 22.2px rgba(0, 0, 0, 0.109), 61px 46px 177px rgba(0, 0, 0, 0.19); display: flex; flex-direction: column; box-sizing: border-box; .marker-card-info { flex: 1; display: flex; padding: 20px 20px 16px; box-shadow: 0px 0px 2px rgba(0, 0, 0, 0.18); margin-bottom: 1px; z-index: 4; .left { margin-right: 20px; display: flex; flex-direction: column; align-items: center; .avator { position: relative; &:before { content: ""; background: linear-gradient( 130deg, #ff7a18, #af002d 41.07%, #319197 76.05% ); position: absolute; top: -5px; left: -5px; width: calc(100% + 10px); height: calc(100% + 10px); z-index: -1; border-radius: 16px; } border-radius: 16px; img { border-radius: 16px; } } } .right { flex: 1; .des { line-height: 36px; } .time { color: royalblue; } } } .like { width: 100%; height: 50px; line-height: 50px; padding: 0 20px; box-sizing: border-box; color: rgba(51, 51, 51, 0.4); background: white; font-size: 14px; display: flex; align-items: center; justify-content: space-between; .left { display: flex; align-items: center; .like-icon { margin-left: 10px; } } } } .fade-enter-active { animation: gbase 0.3s; } .fade-leave-active { animation: gbase 0.3s reverse; } @keyframes gbase { from { opacity: 0; } to { opacity: 1; } } }</style>
使用image方案 写法是比较简单的, 但是背景图过大的话,在移动端显示不出来。为了体验的友好加载性能的问题, 我这边选择了切瓦片图的方案。
切图工具可以选:
MapCutter
tiler_test20221212
两者都可以切图, 但从作者使用上来说,个人更推荐MapCutter 操作比较简单。奈何会员注册有点贵(1000),最后也没办理。
产品需求变更 作者这边选择了 new AMap.ImageLayer() + 矢量图形+ OverlayGroup
点击瓦片图的某个图行,进行一定的交互方式。 同样还是一个vue的文件,以下是代码和写法思路。
<script>import AMap from "../utils/index";import posterBg from "@/assets/poster-drop-animate-bg.png";export default { data() { return { /** 地图 参数 */ zoom: 15, map: null, resMap: null, initLat: 19.94959, //初始维度 initLng: 110.129433, //初始经度 latRank: [19.968605, 19.938321], lngRank: [110.103418, 110.154965], isCoverInfo: {}, overlayGroup: null, circlePoints: [ { lnglat: [110.133991, 19.956492], type: "park", name: "水月公园", des: `并结合现代技术与匠心工艺,诠释中式园林内涵与经典,打造一个具有东方诗情画境的精品滨水公园。`, phone: null, url: null, imgUrl: "", }, ], rectangle: [ { southWest: [110.128716, 19.956905], northEast: [110.133315, 19.955497], name: "xxxxx", id: "rectangle_0_home", des: "光云影间富于变化的旅程。整个文化中心营造杜甫诗中“水流心不竞,云在意俱迟”之境,通过光与水的巧妙融合,表达光之安详及水之智慧,谱写东方人文精神。", phone: "", url: "", imgUrl: "", }, ], polygonHouse: [ { polygon: [ [110.1273, 19.955823], [110.12653, 19.955184], [110.125986, 19.953993], [110.12463, 19.95291], [110.130355, 19.952613], [110.128481, 19.954002], [110.128146, 19.955013], [110.128048, 19.955353], ], name: "xxxxx", id: "rectangle_0_home", des: "标杆性配套项目,实现人才对美好生活的向往。", phone: "400 166 2525", url: "", imgUrl: "", }, ], coverOption: { strokeColor: "", //线颜色#68b990 strokeOpacity: 0.2, //线透明度 strokeWeight: 1, //线粗细度 fillColor: "", //填充颜色#69529d fillOpacity: 0.35, //填充透明度 cursor: "pointer", zIndex: 12, }, mapScale: null, //地图英寸控件' showCoveInfo: false, showMode: false, }; }, mounted() { this.initAMap(); }, computed: { coveInfoImg() { return this.isCoverInfo?.imgUrl || require("@/assets/poster-about.jpg"); }, coveInfoImgArr() { return (isCoverInfo) => { return isCoverInfo.imgUrl || require("@/assets/poster-about.jpg"); }; }, }, methods: { // 创建地图 和 图层 async initAMap() { window.document.title = "地图"; try { this.resMap = await AMap(); this.$nextTick(() => { var nameFormat = "{x}_{y}"; var map = new this.resMap.Map("map", { resizeEnable: true, //是否监控地图容器尺寸变化 center: [this.initLng, this.initLat], zoom: 16, zooms: [15, 18], showLabel: false, }); new this.resMap.TileLayer({ zIndex: 10, getTileUrl: function (x, y, z) { var name = nameFormat .replace("{x}", x) .replace("{y}", y) .replace("{z}", z); return "/mapServe/tiles/" + z + "/" + name + ".png"; }, }).setMap(map); this.map = map; this.mapScale = new this.resMap.Scale({ offset: [20, 47], }); this.map.addControl(this.mapScale); // 设置背景图层 this.addLayer(); //限制地图可视范围 this.lockMapBounds(); // 添加地图点击事件 this.map.on("click", this.mapDefaultClick); // 点聚合集群 // this.markerCluster(); // 添加覆盖物集群 this.addMapOverlayGroup(); }); } catch (e) { console.log(e); } }, addMapOverlayGroup() { /** 公园集合 圆圈 */ let circleGroup = []; /** 旗帜集合 长方形 */ let rectangleGroup = []; /** 楼栋集合 多边形 */ let polygonGroup = []; this.circlePoints.forEach((circleP) => { var circle = new this.resMap.Circle({ ...this.coverOption, center: circleP.lnglat, // 圆心位置 radius: 66, //半径 extData: { name: circleP.name, lnglat: circleP.lnglat, }, }); circle.on("click", () => { this.coverClick(circle, circleP); }); circleGroup.push(circle); }); this.rectangle.forEach((res) => { // 创建矩形覆盖物 var bounds = new this.resMap.Bounds(res.southWest, res.northEast); var rectangleItem = new this.resMap.Rectangle({ bounds: bounds, strokeDasharray: [30, 10], ...this.coverOption, }); rectangleItem.on("click", () => { this.coverClick(rectangleItem, res); }); rectangleGroup.push(rectangleItem); }); // 多边形 this.polygonHouse.forEach((res) => { var polygon = new this.resMap.Polygon({ path: res.polygon, ...this.coverOption, }); polygon.on("click", () => { this.coverClick(polygon, res); }); polygonGroup.push(polygon); }); this.overlayGroup = new this.resMap.OverlayGroup(); this.map.add(this.overlayGroup); this.overlayGroup.addOverlays([ circleGroup, rectangleGroup, polygonGroup, ]); }, // 覆盖物点击事件 coverClick(overlays, coveInfo) { this.isCoverInfo = coveInfo; this.showCoveInfo = true; this.mapScale.hide(); this.coveInfo = this.map.setFitView( overlays, false, [60, 60, 60, 60], 16.5 ); }, // 设置云朵背景图层 addLayer() { var layer = new this.resMap.TileLayer.Flexible({ cacheSize: 30, opacity: 1, zIndex: 8, createTile: function (x, y, z, success, fail) { // if ((x + y) % 3) { // fail(); // return; // } var img = document.createElement("img"); img.onload = function () { success(img); }; img.crossOrigin = "anonymous"; // 必须添加,同时图片要有跨域头 img.onerror = function () { fail(); }; img.src = posterBg; }, }); this.map.addLayer(layer); }, mapDefaultClick(e) { console.log(e); this.showCoveInfo = false; this.isCoverInfo = {}; this.mapScale.show(); }, // 限制地图范围 lockMapBounds() { // var bounds = this.map.getBounds(); var mybounds = new this.resMap.Bounds( [110.104355, 19.964777], [110.163554, 19.925931] ); this.map.setLimitBounds(mybounds); }, // 地图放大一级显示 zoomIn() { this.map.zoomIn(); }, // 地图缩小一级显示 zoomOut() { this.map.zoomOut(); }, // 查看详情 openShowMode() { this.showCoveInfo = false; this.showMode = true; this.mapScale.show(); }, closeShowMode() { this.showMode = false; this.isCoverInfo = {}; this.mapScale.show(); }, goCarWay() { this.$router.push("/bus"); }, },};</script
很明显看到切割后的瓦片图 还有边线+ 左上角的算法坐标(水印)还未去除。
小程序 想要发布访问,只需嵌套地图的线上的url地址就行了。
<web-view :webview-styles="webviewStyles" :src="src"></web-view>
新的一版本需求已经出来, 下期将继续研究 室内地图, 以及室内地图的导航,类似于医院的智慧地图导航。 目前没办法申请高德室内地图的权限 无法推进中。。 欢迎各位大佬指教。