Skip to content

Instantly share code, notes, and snippets.

@Fujihai
Created November 25, 2023 09:59
Show Gist options
  • Save Fujihai/d5ea6065ef2c9eaa0e20386c8bfe03c6 to your computer and use it in GitHub Desktop.
Save Fujihai/d5ea6065ef2c9eaa0e20386c8bfe03c6 to your computer and use it in GitHub Desktop.
在 Vue2 中基于 maptalks 封装 2D 地图
<template>
<div class="map-wrapper">
<div class="drill-info-container">
<div>
<span v-for="(area, idx) in drilledHistory" :key="'drilledArea' + idx">{{ `${area.name}` + `${idx === drilledHistory.length - 1 ? '' : '->'}` }}</span>
</div>
<span class="back-btn" @click="backLastDrilledArea" v-if="drilledHistory.length > 0">返回</span>
</div>
<div id="map" class="container"></div>
<div class="legend-container">
<div class="legend-title">人口密度(人/平方千米)</div>
<div class="legend-density-bar" :style="{ background: `linear-gradient(to right, ${legendRangeColor.min}, ${legendRangeColor.max})`, width: `${this.popDentList.length * 50 || 700}px` }"></div>
<div class="legend-range">
<span>{{ legendRangeValue.min }}</span>
<span>{{ legendRangeValue.max }}</span>
</div>
</div>
</div>
</template>
<script>
import 'maptalks/dist/maptalks.css'
import * as maptalks from 'maptalks'
import { shenzhenArea } from './shenzhen-area'
export default {
name: 'ShenzhenMap',
data () {
return {
map: null,
/**
* 当前地图显示层级,依次为区级、街道和社区
* @typedef {('region'|'street'|'community')}
*/
areaLevel: 'region',
// 区域层级
areaLevelList: Object.freeze(['region', 'street', 'community']),
// 地图多边形图层
areaPolygons: [],
// 区域块图层
polygonsLayer: null,
// 指标显示窗体
areaInfoWindow: null,
// 人口密度色块图
popDentList: [],
// 人口密度区域色阶
heatmapPolygonColorList: Object.freeze(['#82c2f6', '#7cb8ea', '#75aede', '#6fa5d2', '#689bc6', '#6291ba', '#5b87ae', '#557da2', '#4e7496', '#486a8a', '#41607e', '#3b5672', '#344c66', '#2e425a', '#27394e', '#212f42', '#1a2536']),
// 已下钻层级记录,用于下钻及返回处理
drilledHistory: []
}
},
props: {
infoList: {
type: [],
default: () => {
return []
}
},
areaList: {
type: [],
default: () => {
return []
}
},
year: {
type: Number,
default: 2020
}
},
watch: {
infoList: {
immediate: true,
deep: true,
handler (newList, oldList) {
if (!this._.isEqual(newList, oldList)) {
this.genPopDentColorRuleList(newList)
}
}
},
areaList: {
immediate: true,
deep: true,
handler (newList, oldList) {
if (!this._.isEqual(newList, oldList)) {
const _len = this.drilledHistory.length
if (_len > 0) {
this.addDrilledAreaPolygons()
// 处理下钻缩放 & 中心点控制
let _zoom = 2
if (this.areaLevel === 'community') {
_zoom = 6
}
if (this.map) {
this.map.setZoom(_zoom)
this.map.setCenter(this.drilledHistory[_len - 1].center)
}
} else {
if (newList.length > 0) {
this.initShenzhenPolygons()
}
}
}
}
},
year: function (val) {
this.onPageSwitch()
},
$route (to, from) {
this.onPageSwitch()
}
},
computed: {
legendRangeColor () {
const _len = this.popDentList.length
const _hmLen = this.heatmapPolygonColorList.length
if (_len > 1) {
return {
min: this.heatmapPolygonColorList[0],
max: this.heatmapPolygonColorList[_len - 1]
}
}
return {
min: this.heatmapPolygonColorList[0],
max: this.heatmapPolygonColorList[_hmLen - 1]
}
},
legendRangeValue () {
const _len = this.popDentList.length
if (_len > 0) {
return {
min: this.popDentList[0].start || 0,
max: this.popDentList[_len - 1].end || '-'
}
}
return {
min: '--',
max: '--'
}
}
},
methods: {
/**
* 页面切换后需要重置
*/
onPageSwitch () {
this.clearAreaPolygonLayer()
this.resetMap()
this.initShenzhenPolygons()
},
/**
* 处理 areaLevel 变量
*/
rollbackAreaLevel () {
if (this.drilledHistory.length === 3) {
this.areaLevel = 'street'
} else if (this.drilledHistory.length === 2) {
this.areaLevel = 'region'
} else if (this.drilledHistory.length === 1) {
this.$emit('drillend')
this.resetMap()
this.initShenzhenPolygons()
}
},
/**
* 下钻返回处理
*/
backLastDrilledArea () {
this.hideAreaInfoWindow()
this.rollbackAreaLevel()
// 返回当前层级
if (this.drilledHistory.length > 1) {
this.drilledHistory.pop()
}
// 再返回上一层,重新下钻
if (this.drilledHistory.length > 0) {
const _lastDrilledArea = this.drilledHistory.pop()
this.onAreaDblClick(_lastDrilledArea, true)
}
},
/**
* 重置地图
*/
resetMap () {
// 重置相关数据
this.areaLevel = 'region'
this.popDentList = []
this.drilledHistory = []
// 重置地图中心点和缩放比例
this.map && this.map.setCenter(new maptalks.Coordinate([114.476164, 22.647248]))
this.map && this.map.setZoom(0.9)
// 隐藏信息窗体
if (this.areaInfoWindow) {
this.hideAreaInfoWindow()
}
},
/**
* 根据最大最小值动态生成热力图分层色块规则
* @param { Number } min - 最小值
* @param { Number } max - 最大值
* @param { Number } layers - 区间数
* @todo 现阶段最小值为 0,在 [较人普对比页面] 人口密度可能存在负数,分层规则不适用
*/
getRangeRuleList (min = 0, max = 100, layers = 15) {
const rules = []
if (max < layers) {
for (let i = 0; i < max; i++) {
rules.push({
start: i,
end: i + 1
})
}
} else if (max > layers) {
const base = parseInt(max / layers)
for (let i = 0; i < layers; i++) {
rules.push({
start: i * base,
end: i === layers - 1 ? Math.max((i + 1) * base, max) : (i + 1) * base
})
}
}
return rules
},
/**
* 获取深圳各区区域块颜色
* @param { Number | String } - adcode - 区码
*/
getRegionPolygonColor (adcode) {
if (this.areaList.length === 0) {
return 'rgba(135, 196, 240,0.6)'
}
const foundItem = this.areaList.find(area => Number(area.code) === Number(adcode) || area.code.indexOf(adcode.toString()) > -1)
if (!foundItem) {
return 'rgba(135, 196, 240,0.6)'
}
return this.getPolygonColor(foundItem.density)
},
/**
* 获取区域块的颜色
* @param { Number } - popDent人口密度数值
*/
getPolygonColor (popDent) {
if (this.popDentList.length === 0 || !popDent) {
return 'transparent'
}
let color = 'transparent'
this.popDentList.forEach((rule, idx) => {
if (popDent >= rule.start && popDent <= rule.end) {
color = this.heatmapPolygonColorList[idx]
}
})
return color
},
/**
* 生成人口密度热力图色块数组
* 1. 按 [POPULATION_DENSITY] 字段升序,取得 min, max 值
* 2. 通过 min, max 值生成区间,区间个数与显示区域个数一致
* @param { Object } - infoList 区块信息数组
*/
genPopDentColorRuleList (infoList = []) {
if (infoList.length === 0) {
return
}
// 升序
const _ascCompare = property => {
return function (objA, objB) {
return Number(objA[property]) - Number(objB[property])
}
}
const _copyList = this._.cloneDeep(infoList)
_copyList.sort(_ascCompare('POPULATION_DENSITY'))
const len = _copyList.length
if (len < 2) {
return
}
const _max = _copyList[len - 1]['POPULATION_DENSITY']
const _min = _copyList[0]['POPULATION_DENSITY']
this.popDentList = this.getRangeRuleList(_min, _max, len)
},
/**
* 获取区块显示指标映射,用于区域指标信息窗体显示
* @todo 六、七人普及比较页面指标映射不完全一致,当前未做区分,后续可能需要调整
*/
getAreaInfoMap (year = 2020, isCompare = false) {
return [
{ title: '行政区划名称', key: 'REGION_NAME', unit: '' },
{ title: '人口密度', key: 'POPULATION_DENSITY', unit: '人/平方千米' },
{ title: '常住人口', key: 'RESIDENT', unit: '万人' },
{ title: '男性人口', key: 'MALE_RESIDENT', unit: '万人' },
{ title: '女性人口', key: 'FEMALE_RESIDENT', unit: '万人' },
{ title: '常住户籍人口', key: 'RESIDENT_HJ', unit: '万人' },
{ title: '常住非户籍人口', key: 'RESIDENT_FHJ', unit: '万人' },
{ title: '家庭户户数', key: 'HOUSEHOLD', unit: '万户' },
{ title: '家庭户人口', key: 'HOUSEHOLD_POP', unit: '万人' },
{ title: '平均每个家庭户人口', key: 'AVG_HOUSEHOLD_POP', unit: '万人' },
{ title: '集体户户数', key: 'COLLECTIVE_HOUSEHOLD', unit: '万户' },
{ title: '集体户人口', key: 'COLLECT_HOUSEHOLD_POP', unit: '万人' },
{ title: '人户分离人口', key: 'SEPARATE_RESIDENT', unit: '万人' },
{ title: '市辖区内人户分离人口', key: 'CITYAREA_SEPARATE_RESIDENT', unit: '万人' },
{ title: '流动人口', key: 'FLOAT_RESIDENT', unit: '万人' }
]
},
/**
* 绘制下钻区域
*/
addDrilledAreaPolygons () {
if (['street', 'community'].includes(this.areaLevel)) {
this.areaPolygons = []
const labels = []
this.areaList.forEach(area => {
if (area.borders instanceof Array && area.borders.length > 0) {
area.borders.forEach(borders => {
// 绘制边界
// eslint-disable-next-line camelcase
const { border_points, border_center_lat, border_center_lng } = area.borders[0]
const { code, name } = area
const _polygonCfg = {
name,
adcode: code,
center: [Number(border_center_lng), Number(border_center_lat)]
}
let polygon = new maptalks.Polygon(JSON.parse(border_points), {
visible: true,
editable: false,
cursor: 'pointer',
shadowBlur: 0,
shadowColor: 'black',
draggable: false,
dragShadow: false,
drawOnAxis: null,
symbol: {
'lineColor': 'white',
'lineWidth': 1,
'polygonFill': this.getPolygonColor(area.density)
},
extraCfg: _polygonCfg
})
polygon.on('dblclick', this.onAreaDblClick)
polygon.on('click', this.onAreaClick)
this.areaPolygons.push(polygon)
// 文字标记生成
// eslint-disable-next-line camelcase
let label = new maptalks.Label(name.replace(/省|市|办事处|居委会/gi, ''), [Number(border_center_lng), Number(border_center_lat)], {
'draggable': true,
'textSymbol': {
'textFaceName': 'monospace',
'textFill': '#34495e',
'textHaloFill': '#fff',
'textHaloRadius': 4,
'textSize': 18,
'textWeight': 'bold',
'textVerticalAlignment': 'top'
}
})
labels.push(label)
})
}
})
if (this.polygonsLayer) {
this.polygonsLayer.clear()
}
this.polygonsLayer.addGeometry([...this.areaPolygons, ...labels]).addTo(this.map)
}
},
/**
* 下钻至下一层级
* 七人普支持层级区域下钻,六人普及比较页面只支持下钻至街道
*/
drill2NextAreaLevel () {
const levelLimit = this.year === 2020 ? 'community' : 'street'
const curLevelIdx = this.areaLevelList.findIndex(area => area === this.areaLevel)
if (this.areaLevel !== levelLimit && curLevelIdx < this.areaLevelList.length - 1) {
this.areaLevel = this.areaLevelList[curLevelIdx + 1]
}
},
/**
* WMTS 底图服务初始化
*/
initMap () {
let resolutions = [0.001373291015625, 6.866455078125E-4, 3.433227539063E-4, 1.716613769531E-4, 8.58306884766E-5, 4.29153442383E-5, 2.14576721191E-5, 1.07288360596E-5, 5.3644180298E-6, 2.6822090149E-6, 1.3411045074E-6]
let mapurl = 'http://10.253.102.69/gw/OGC/Map/SZ_VEC_B4490/?LAYER=w_shenzhen&style=&SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&TILEMATRIXSET=EPSG%3A4490&FORMAT=image%2Fpng&TILEMATRIX=EPSG%3A4490%3A{z}&TileRow={y}&TileCol={x}&szvsud-license-key=ndLg0mxz3dbSZiGU5izQ4iGI2cdx/8YGya02K9UgrviZHIR/LZ72feWTJVJsOsYv'
this.map = new maptalks.Map('map', {
center: [114.477026, 22.615358],
zoom: 0.9,
minZoom: 0.9,
maxZooom: 6,
pitch: 0,
spatialReference: {
projection: 'EPSG:4326',
resolutions: resolutions
},
attribution: false,
baseLayer: new maptalks.TileLayer('base', {
tileSize: [256, 256],
tileSystem: [1, -1, -180, 90],
subdomains: ['1', '2', '3', '4', '5'],
urlTemplate: mapurl
})
})
},
/**
* 绘制深圳各区区域块,边界信息来自于规自局
*/
initShenzhenPolygons () {
const { geometries } = shenzhenArea
const polygons = []
const labels = []
for (let i = 0, len = geometries.length; i < len; i++) {
const { coordinates } = geometries[i]
const { name, adcode, center } = geometries[i].properties
const _polygonCfg = {
name,
adcode,
center
}
// 区域块生成
let polygon = new maptalks.MultiPolygon(coordinates, {
visible: true,
editable: false,
cursor: 'pointer',
shadowBlur: 0,
shadowColor: 'black',
draggable: false,
dragShadow: false,
drawOnAxis: null,
symbol: {
'lineColor': 'black',
'lineWidth': 1,
// 'polygonFill': 'rgba(135,196,240,0.6)',
// 'polygonOpacity': 0.2,
'polygonFill': this.getRegionPolygonColor(adcode)
},
extraCfg: _polygonCfg
})
polygon.on('dblclick', this.onAreaDblClick)
polygon.on('click', this.onAreaClick)
polygons.push(polygon)
// 文字标记生成
let label = new maptalks.Label(name.replace(/省|市|办事处|居委会/gi, ''), center, {
'draggable': true,
'textSymbol': {
'textFaceName': 'monospace',
'textFill': '#34495e',
'textHaloFill': '#fff',
'textHaloRadius': 4,
'textSize': 18,
'textWeight': 'bold',
'textVerticalAlignment': 'top'
}
})
labels.push(label)
}
if (this.polygonsLayer) {
this.polygonsLayer.clear()
this.map && this.map.removeLayer(this.map.getLayers())
} else {
this.polygonsLayer = new maptalks.VectorLayer('vector')
}
this.polygonsLayer && this.polygonsLayer.addGeometry([...polygons, ...labels])
this.polygonsLayer.addTo(this.map)
},
/**
* 从区域信息列表中匹配出当前点击区域的信息项
*/
getMatchedAreaInfo (cfg = null) {
if (!cfg || this.infoList.length === 0) {
return null
}
return this.infoList.find(info => info.REGION_NAME.replace(/省|市|区|办事处/gi, '') === cfg.name.replace(/省|市|区|办事处/gi, ''))
},
/**
* 添加下钻区域项
* @param { Object } extraCfg = { adcode: '44030000000', name: '深圳市' } 为绘制区域块的区域信息参数
*/
addDrillHistoryItem (extraCfg = null) {
if (!extraCfg) {
return
}
const _found = this.drilledHistory.find(ele => ele.adcode === extraCfg.adcode)
if (_found) {
return
}
this.drilledHistory.push(extraCfg)
},
/**
* 清除当前区域边界图层
*/
clearAreaPolygonLayer () {
if (this.areaLevel !== 'community') {
this.polygonsLayer && this.polygonsLayer.clear()
}
},
/**
* 单击区域块显示指标信息窗体
* https://maptalks.org/maptalks.js/api/0.x/Polygon.html#event:click
* @param { Object } - maptalks polygon 单击事件回调参数
*/
onAreaClick (param) {
this.$emit('showInfo', param.target.options.extraCfg)
this.showAreaInfoWindow(param)
},
/**
* 双击区域块进行下钻显示
* @param { Object } param - maptalks polygon 双击事件回调参数 | 用户传递的下钻参数
* @param { Boolean } isBackDrill - 是否为 [下钻返回] 操作
*/
onAreaDblClick (param, isBackDrill = false) {
this.hideAreaInfoWindow()
const _param = isBackDrill ? param : param.target.options.extraCfg
if (!_param) {
return
}
this.$emit('drill', _param)
this.addDrillHistoryItem(_param)
this.clearAreaPolygonLayer()
this.drill2NextAreaLevel()
},
/**
* 隐藏区域指标信息窗体
*/
hideAreaInfoWindow () {
const popInfoWindow = document.querySelector('.pop-info-window-wrapper')
if (popInfoWindow && popInfoWindow.style.display !== 'none') {
popInfoWindow.style.display = 'none'
}
},
/**
* 显示区域指标信息窗体
*/
showAreaInfoWindow (param = null) {
if (this.areaInfoWindow) {
this.hideAreaInfoWindow()
}
if (!param) {
return
}
let cfg = param.target.options.extraCfg
const _matchedItem = this.getMatchedAreaInfo(cfg)
if (!_matchedItem) {
return
}
const coordinate = param.coordinate
const _infoMap = this.getAreaInfoMap()
let _html = ''
_matchedItem && (_infoMap.map(info => {
_html += `<div class="info-item-container"><span class="title">${info.title}:</span><span class="value">${_matchedItem[info.key] ? _matchedItem[info.key] + ' ' + info.unit : '-'}</span></div>`
}))
var options = {
'single': true,
'width': 183,
'height': 105,
'custom': true,
'dx': -3,
'dy': -12,
'amination': 'fade',
'eventsPropagation': false,
'eventsToStop': false,
'content': `<div class="pop-info-window-wrapper">${_html}</div>`
}
this.areaInfoWindow = new maptalks.ui.InfoWindow(options)
this.areaInfoWindow.addTo(this.polygonsLayer).show(coordinate)
}
},
mounted () {
this.$nextTick(() => {
this.initMap()
})
}
}
</script>
<style lang='scss' scoped>
html,body{ margin:0px;height:100%;width:100%; }
.container{ width:100%;height:100% }
.map-wrapper {
height: 100%;
width: 100%;
position: relative;
.legend-container {
position: absolute;
bottom: 48px;
right: 64px;
.legend-title {
font-size: 32px;
color: #BCDDF7;
margin-bottom: 18px;
}
.legend-density-bar {
height: 24px;
width: 700px;
margin-bottom: 18px;
}
.legend-range {
display: flex;
font-size: 24px;
justify-content: space-between;
}
}
.drill-info-container {
font-size: 40px;
display: flex;
font-family: "FZZY-GBK";
margin-bottom: 16px;
div {
flex: 1;
}
.back-btn {
padding: 8px 24px;
background: rgba(45, 153, 255, 0.8);
cursor: pointer;
}
}
}
</style>
<style lang="scss">
@import '../../assets/scss/mixins';
.pop-info-window-wrapper {
background-image: url('./imgs/map-tooltip-wrapper.png');
@include background-image-fit;
width: 400px;
padding: 32px 22px 22px 22px;
position: relative;
.info-item-container {
display: flex;
span.title {
flex: 1;
display: inline-block;
}
span.value {
flex: 1;
display: inline-block;
text-align: right;
}
}
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment