概述
MapboxGL热力图的配置参数并不多,但是有时候为了或得一个比较好用的热力图配置参数,我们不得不改代码再预览,显得尤为麻烦,为方便配置,实现实时预览,本文使用ace实现了一个热力图样式在线配置页面。
效果
实现
1. 技术栈
- Vue3 + Element Plus
- ace Editor
- mapboxGL
2. 实现功能
- csv、json、geojson数据上传并解析
- mapboxGL热力图
- 热力图样式编辑与实时预览
3. 实现
3.1 交互界面
<template><div class="tips"><b>说明:</b>实现热力图样式的配置与预览。</div><div class="container"><div class="setting-panel"><div class="title">配置参数</div><div class="content"><el-formlabel-width="0":model="styleFormData"><el-form-item label=""><div class="label">Radius<div class="tooltip"><el-icon><InfoFilled /></el-icon><div class="tooltips"><p>Radius of influence of one heatmap point in pixels. Increasing the value makes the heatmap smoother, but less detailed.</p></div></div></div><el-input class="my-input" size="small" :rows="2" type="textarea" v-model="styleFormData.radius" /></el-form-item><el-form-item label=""><div class="label">Color<div class="tooltip"><el-icon><InfoFilled /></el-icon><div class="tooltips"><p>Defines the color of each pixel based on its density value in a heatmap. Should be an expression that uses ["heatmap-density"] as input.</p></div></div></div><el-input class="my-input" size="small" :rows="4" type="textarea" v-model="styleFormData.color" /></el-form-item><el-form-item label=""><div class="label">Weight<div class="tooltip"><el-icon><InfoFilled /></el-icon><div class="tooltips"><p>A measure of how much an individual point contributes to the heatmap. A value of 10 would be equivalent to having 10 points of weight 1 in the same spot. Especially useful when combined with clustering.</p></div></div></div><el-input class="my-input" size="small" :rows="2" type="textarea" v-model="styleFormData.weight" /></el-form-item><el-form-item label=""><div class="label">Intensity<div class="tooltip"><el-icon><InfoFilled /></el-icon><div class="tooltips"><p>Similar to `heatmap-weight` but controls the intensity of the heatmap globally. Primarily used for adjusting the heatmap based on zoom level.</p></div></div></div><el-input class="my-input" size="small" :rows="2" type="textarea" v-model="styleFormData.intensity" /></el-form-item><el-form-item label=""><div class="label">Opacity<div class="tooltip"><el-icon><InfoFilled /></el-icon><div class="tooltips"><p>The global opacity at which the heatmap layer will be drawn.</p></div></div></div><el-input class="my-input" size="small" :rows="2" type="textarea" v-model="styleFormData.opacity" /></el-form-item></el-form></div><div class="title">JSON编辑器<div class="tools"><el-button size="small" @click="copyStyle">复制</el-button></div></div><div class="content code" id="codeEditor"></div></div><div class="main-panel"><div class="data-panel"><el-uploaddragref="file"action="''":multiple="false":auto-upload="false":limit="1":on-exceed="handleExceed":on-change="changeDataFile":accept="'.csv,.json,.geojson'"><div class="el-upload__text">拖动文件到此或 <em>点击上传</em></div><template #tip><div class="el-upload__tip">可上传csv、json、geojson等格式点数据,如为csv、json需包含lon,lat字段,如添加<b style="color: red">权重</b>,需<b style="color: red">值</b>字段</div></template></el-upload></div><MapComponent :is-tools="false" @map-loaded="mapLoaded" style="height: 100%;"></MapComponent></div></div>
</template><style scoped lang="scss">
@import "../../assets/common/style";
.container {margin-top: 1.5rem;height: calc(100% - 3.5rem);position: relative;display: flex;flex-direction: row;.main-panel {flex-grow: 1;height: calc(100% - 1.8rem);position: relative;.data-panel {padding: 0.8rem;background-color: white;position: absolute;top: 1rem;right: 1rem;z-index: 99;width: 25rem;}}.setting-panel {width: 25rem;height: 100%;box-shadow: 0 0 5px #ccc;box-sizing: border-box;margin-right: 1.5rem;}.title {padding: 0.6rem 1.2rem;font-weight: bold;font-size: 1.1rem;border: 1px solid #efefef;}.content {padding: 1.2rem 1.2rem 0 1.2rem;&.code {height: calc(100% - 33.7rem)}}.tools {float: right;}.label, .my-input {display: inline-block;width: calc(100% - 7rem);.el-input__wrapper {width: 100%;}}.label {width: 6rem;height: 100%;line-height: 1.8;text-align: right;padding-right: 0.6rem;}.tooltip {display: inline-block;cursor: pointer;position: relative;&:hover {.tooltips {display: block;}}.tooltips {display: none;position: absolute;left: -8px;top: 22px;background-color: rgba(0,0,0,0.6);color: #fff;border-radius: 3px;z-index: 999;padding: 0.5rem;width: 17rem;white-space: normal;font-size: 12px;p {width: 100%;word-break: break-word;margin: 0;text-align: left;line-height: 1.5;}&:before {content: ' ';width: 0;height: 0;border: 5px solid transparent;border-bottom-color: rgba(0,0,0,0.6);position: absolute;left: 10px;top: -10px;}}}
}
</style>
3.2 数据上传与解析
changeDataFile(file, fileList) {uploadFile = filethis.showData()
},
handleExceed(files) {this.$refs.file.clearFiles()this.$refs.file.handleStart(files[0])
},
showData() {const that = thisif(!uploadFile) {ElMessage({message: '未上传文件!',type: 'warning',})return}const fileType = uploadFile.name.split('.')[1]const reader = new FileReader();reader.readAsText(uploadFile.raw,'GB2312');reader.onload = function () {const fileContent = reader.result;let geojson = nullif(fileType === 'csv') {let {geomType, features} = csv2geojson(fileContent)geomType = geomType.toLowerCase()if (geomType.indexOf('point') !== -1) {geojson = new Geojson(features)}} else if(fileType === 'json') {let {geomType, features} = json2Geojson(JSON.parse(fileContent))geomType = geomType.toLowerCase()if (geomType.indexOf('point') !== -1) {geojson = new Geojson(features)}} else {geojson = JSON.parse(fileContent)}if(geojson) {map.getSource(`${DATA_LAYER}-source`).setData(geojson);that.styleUpdate()const [xmin, ymin, xmax, ymax] = turf.bbox(geojson);const bbox = [[xmin, ymin], [xmax, ymax]];map.fitBounds(bbox, {padding: {top: 100, bottom:100, left: 150, right: 150},duration: 500})}}
},
csv2geojson和json2Geojson转换方法如下:
import {Feature} from './geojson'
import { wktToGeoJSON } from "@terraformer/wkt"export function csv2geojson(csvContent) {const splitChar = csvContent.indexOf('\r') ? '\r' : '\r\n'const lines = csvContent.split(splitChar).filter(v => Boolean(v))const headers = lines[0].split(',').map(header => header.toLowerCase())let geomType = '', features = [], isWkt = falseif(headers.includes('lon') && headers.includes('lat')) {geomType = 'Point'} else if(headers.includes('wkt')) {isWkt = trueconst geom = wktToGeoJSON(lines[1].split(',')[headers.indexOf('wkt')])geomType = geom.type}if(geomType) {for (let i = 1; i < lines.length; i++) {const line = lines[i].split(',')if(line.length === headers.length) {let props = {}headers.forEach((header, index) => {if(!['wkt', 'lon', 'lat'].includes(header)) props[header] = line[index]})const lonIndex = headers.indexOf('lon')const latIndex = headers.indexOf('lat')const geometry = isWkt ? wktToGeoJSON(line[headers.indexOf('wkt')]) : [line[lonIndex], line[latIndex]].map(Number)features.push(new Feature(geomType, props, geometry))}}}return {headers,geomType,features}
}export function json2Geojson(json) {if(!Array.isArray(json)) throw new Error('数据格式错误')const geomType = 'Point'const features = json.map(d => {const {lon, lat} = dreturn new Feature(geomType, d, [lon, lat])})return {geomType,features}
}
3.3 样式编辑与实时预览
initEditor() {editor = ace.edit("codeEditor");const theme = "github";const language = "json";editor.setTheme("ace/theme/" + theme);editor.session.setMode("ace/mode/" + language);editor.setFontSize(14);editor.setReadOnly(false);editor.setOption("wrap", "free");editor.setOptions({enableBasicAutocompletion: true,enableSnippets: true,enableLiveAutocompletion: true,tabSize: 2});this.styleUpdate()
},
styleUpdate() {const style = {"heatmap-radius": this.styleFormData.radius,"heatmap-color": this.styleFormData.color,"heatmap-weight": this.styleFormData.weight,"heatmap-intensity": this.styleFormData.intensity,"heatmap-opacity": this.styleFormData.opacity,}let isOk = truefor (const styleKey in style) {let val = style[styleKey]if(typeof val === 'string') val = val.replace(/'/g, '"')if(val === '') isOk = falseif(styleKey !== 'heatmap-color' && ! Number.isNaN(Number(val))) style[styleKey] = Number(vaelse style[styleKey] = JSON.parse(val || '{}')if(styleKey === 'heatmap-opacity' && style[styleKey] > 1) style[styleKey] = 1if(styleKey === 'heatmap-opacity' && style[styleKey] < 0) style[styleKey] = 0}if(isOk) {editor.setValue(JSON.stringify(style, null, 2))if(window.map) {if(map.getLayer(`${DATA_LAYER}-layer`)) map.removeLayer(`${DATA_LAYER}-layer`)map.addLayer({id: `${DATA_LAYER}-layer`,type: "heatmap",source: `${DATA_LAYER}-source`,paint: style});}}
},