今天來記錄一篇關于h3.js插件庫的使用,他可以很高效的計算出地球上某個經緯度坐標六邊形頂點。
前段時間領導突然給我個售前功能,要求是使用h3.js插件在地球上繪制出六邊形網格碼,本來以為挺棘手的,結果看完文檔后發現也挺簡單的,話不多說,開干...
h3.js
學前熱身
首先看幾張示意圖:
1. 基礎的六角網格
2. 可根據地球縮放層級的不同而渲染不同大小的六邊形
3. 右擊網格碼,可以變換底色
4. 可以根據隨機給六角網格添加顏色區分
?實現思路
準備
首先給項目安裝相關依賴
npm install h3-js
或者
pnpm install h3-js
?當然如果我們不是vue等框架項目,也無所謂,只需要引入h3.js的js文件即可。
<script src="https://unpkg.com/h3-js"></script>
?實現
useHexagonTrellisCode
?是一個基于 Cesium 和 H3 庫的六邊形網格碼可視化工具,用于在地圖上生成和顯示 H3 六邊形網格系統。
其中我們實現時需要用到幾個h3.js自帶的api,大家想了解的可以去官網看看。
polygonToCells, cellToLatLng, latLngToCell, cellToBoundary, getHexagonEdgeLengthAvg, UNITS
?我將本次編輯的代碼整合到一個類中,下面是使用方法:
初始化
import * as Cesium from 'cesium';
import { useHexagonTrellisCode } from './useHexagonTrellisCode';// 創建Cesium Viewer
const viewer = new Cesium.Viewer('cesiumContainer');// 初始化配置,參數下面有簡介
let option = {level: 6, // 網格層級 (3-6)polygonArea: [ // 初始繪制區域,注意這里是緯度在前,經度在后[緯度1, 經度1],[緯度2, 經度2],[緯度3, 經度3]],isSort: true, // 是否對六邊形進行排序(可選)Mark_color: ["#00ffff85"] // 標記顏色(可選)
}// 初始化六邊形網格碼工具
const hexagonTool = new useHexagonTrellisCode(viewer, option);// 是否顯示標簽(可選)
hexagonTool.showLabel();// 渲染網格
hexagonTool.render();
參數配置(option)
參數 | 類型 | 默認值 | 說明 |
---|---|---|---|
level | number | 6 | H3網格層級 (3-6) |
isPolygonFullDraw | boolean | false | 是否根據視口大小繪制圖形(還在實驗中) |
isSort | boolean | false | 是否對六邊形進行排序 |
Mark_color | string[] | ["#00ffff85", "#a605ff85", "#ffce0585", "#05ff1d85"] | 標記顏色數組 |
polygonArea | number[][] | [] | 定義繪制六邊形網格碼的區域 |
?主要方法
1、顯示標簽
showLabel()
2、渲染網格
render()
3、高亮選中的六邊形
/*** longitude: 經度* latitude: 緯度* option: 可選配置,如 { selectColor: "red" }*/
select_high_lightObject(longitude, latitude, option)
4、設置網格層級
setLevel(level)
5、設置繪制區域
setPolygonArea(position)
6、銷毀實例
destory()
使用示例
1、基本使用
const useHexagonTrellis = new useHexagonTrellisCode(viewer, {level: 5,polygonArea: [[39.9, 116.3],[39.9, 116.5],[40.1, 116.5],[40.1, 116.3]]
}).showLabel().render();
2、交互示例
// 點擊地圖高亮六邊形
viewer.screenSpaceEventHandler.setInputAction(function(movement) {const pickedObject = viewer.scene.pick(movement.endPosition);if (pickedObject && pickedObject.id) {const cartographic = Cesium.Cartographic.fromCartesian(pickedObject.id);const longitude = Cesium.Math.toDegrees(cartographic.longitude);const latitude = Cesium.Math.toDegrees(cartographic.latitude);useHexagonTrellis.select_high_lightObject(longitude, latitude, {selectColor: "#ff0000"});}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
3、動態調整層級
viewer.camera.changed.addEventListener(() => {const height = viewer.camera.positionCartographic.height;if (viewer.scene.mode === Cesium.SceneMode.SCENE3D) {let level = Math.floor(Math.log2(2.0e7 / height));useHexagonTrellis.setLevel(level);}
})
主要代碼
import * as Cesium from 'cesium'
import { polygonToCells, cellToLatLng, latLngToCell, cellToBoundary, getHexagonEdgeLengthAvg, UNITS } from "h3-js";
export class useHexagonTrellisCode {viewer = null;hexgonIds = [];/*** 所有六邊形圖元*/PrimitiveMain = null;/*** 選中時所顯示的圖元*/selectPrimitive = null;/*** 編碼標簽的集合*/labelsCollection = null;/*** 標識圓的所有圖元集合*/_circles = [];config = {level : 6, // 當前層級isPolygonFullDraw: false, // 是否根據視口大小繪制圖形isSort: false, // 是否排序Mark_color: ["#00ffff85", "#a605ff85", "#ffce0585", "#05ff1d85"], // 是否在六邊形上添加透明圖元polygonArea: [], // 定義繪制六邊形網格碼的區域}/*** useHexagonTrellisCode 生成六邊形網格碼* @param {*} viewer * @param {*} option * @returns */constructor (viewer, option) {this.viewer = viewer;this.config = {...this.config,...option};return false;}/*** 開啟標簽顯示* @returns */
/*** longitude: 經度* latitude: 緯度* option: 可選配置,如 { selectColor: "red" }*/showLabel() {this.labelsCollection && this.viewer.scene.primitives.remove(this.labelsCollection);this.labelsCollection = new Cesium.LabelCollection();this.viewer.scene.primitives.add(this.labelsCollection);return this;}/*** 渲染* @returns */render() {if (this.config.polygonArea.length == 0) {return false;}this.clearOldPrimitive();const hexagons = polygonToCells(this.config.polygonArea, this.config.level);this.hexgonIds = hexagons;let _hexagons_center = hexagons;// 添加排序if (this.config.isSort) {let _hexagons = hexagons.map(hexId => {const center = cellToLatLng(hexId);return {hexId, lon:center[1], lat: center[0]}})_hexagons_center = _hexagons.sort((a, b) => {if (a.lat !== a.lat) return b.lat - a.lat;return a.lon - b.lon;}).map(t => t.hexId)}this.PrimitiveMain = this.createPrimitive_line();let instances = [];let drawNum = 0;_hexagons_center.forEach((hexId) => {instances.push(this.createHexagonInstance_line(hexId))if (this.config.Mark_color.length > 0 && drawNum++ < this.config.Mark_color.length) {this.drawCircles(hexId, this.config.Mark_color[drawNum]);}if (this.labelsCollection) this.addLabel(hexId);})this.PrimitiveMain.geometryInstances = instances;return this.viewer.scene.primitives.add(this.PrimitiveMain);}/*** 根據視口繪制六邊形* 實驗中...*/drawPolygonByViewFull() {// 判斷選中區域是否在視口內let isPolygonFull = this.isPolygonFullyInViewport(this.config.polygonArea);if (isPolygonFull == 0) {console.log("111")return false;}if (isPolygonFull == 2) {console.log("222")return this.render();}console.log("333")const viewRectangle = this.viewer.camera.computeViewRectangle();let position = [[Cesium.Math.toDegrees(viewRectangle.west),Cesium.Math.toDegrees(viewRectangle.south)],[Cesium.Math.toDegrees(viewRectangle.east),Cesium.Math.toDegrees(viewRectangle.south)],[Cesium.Math.toDegrees(viewRectangle.east),Cesium.Math.toDegrees(viewRectangle.north)],[Cesium.Math.toDegrees(viewRectangle.west),Cesium.Math.toDegrees(viewRectangle.north)]]this.removeLabelCollection();const _hexagons = polygonToCells(position, this.config.level);let instances = [];this.hexgonIds.forEach(hexId => {if (_hexagons.includes(hexId)) {instances.push(this.createHexagonInstance_line(hexId))if (this.labelsCollection) this.addLabel(hexId);}})this.PrimitiveMain.geometryInstances = instances;return true;}/*** 添加label標簽* @param {*} hexId * .padEnd(this.config.labelLenght || 5, '0')*/addLabel(hexId){let lonlat = cellToLatLng(hexId);this.labelsCollection.add({position : Cesium.Cartesian3.fromDegrees(lonlat[1], lonlat[0]),text : hexId.replace(/[^\d]/g,''),font : `${this.config.labelSize || 16}px sans-serif`,horizontalOrigin : Cesium.HorizontalOrigin.CENTER,verticalOrigin : Cesium.VerticalOrigin.BOTTOM,})}/*** 通過經緯度添加選中效果* @param {*} longitude * @param {*} latitude * @param {*} option * @returns */select_high_lightObject(longitude, latitude, option) {if(!longitude || !latitude || this.config.level == -1) {return false;}let _code = latLngToCell(latitude, longitude,this.config.level);if (this.hexgonIds.includes(_code)) {// 刪除之前高亮圖元this.viewer.scene.primitives.remove(this.selectPrimitive); this.selectPrimitive = this.createPrimitive_polygon();this.selectPrimitive.geometryInstances = [ this.createHexagonInstance_polygon(_code, option.selectColor || "red") ];this.viewer.scene.primitives.add(this.selectPrimitive);return true;}else {return false;}}/*** 繪制圓* @param {*} hexId */drawCircles(hexId, color) {// 繪制填充多邊形let polygon = this.createPrimitive_polygon();let geomtry = this.createHexagonInstance_polygon(hexId, color || "red");polygon.geometryInstances = geomtry;this._circles.push(polygon);this.viewer.scene.primitives.add(polygon);// 繪制圓let _circle = this.createPrimitive_circle();let instance = [];let distance = parseInt(getHexagonEdgeLengthAvg(this.config.level, UNITS.m)) * (Math.random() * (0.9 - 0.2) + 0.2);let colors = ["#098", "#af2","#f2a","#b2a"];for (let i=0; i<4; i++){let radius = distance - (distance * (i / 5));// 使用疊加法繪制原型輪廓線for (let j=0; j< 30; j++){instance.push(this.createHexagonInstance_circle(hexId, radius - j,colors[i]))}}_circle.geometryInstances = instance;this._circles.push(_circle);this.viewer.scene.primitives.add(_circle);}/*** 刪除選中后添加的高亮圖元*/remove_select_primitive() {this.selectPrimitive && this.viewer.scene.primitives.remove(this.selectPrimitive);}/*** 添加線類型Primitive* @returns */createPrimitive_line() {return new Cesium.Primitive({geometryInstances: [],appearance: new Cesium.PolylineColorAppearance({translucent: false,renderState: {depthTest: { enabled: false }, // 關閉深度測試blending: Cesium.BlendingState.ALPHA_BLEND,depthMask: false, // 關鍵:禁止寫入深度緩沖區}}),asynchronous: false});}/*** 添加圓類型Primitive* @returns */createPrimitive_circle() {return new Cesium.Primitive({geometryInstances: [],appearance: new Cesium.PerInstanceColorAppearance({translucent: false,closed: false,flat: true,renderState: {depthTest: { enabled: false }, // 關閉深度測試blending: Cesium.BlendingState.ALPHA_BLEND,depthMask: false // 關鍵:禁止寫入深度緩沖區}}),asynchronous: false});}/*** 添加多邊形類型Primitive* @returns */createPrimitive_polygon() {return new Cesium.Primitive({geometryInstances: [],appearance: new Cesium.PerInstanceColorAppearance({translucent: false,closed: false,renderState: {depthTest: { enabled: false }, // 關閉深度測試blending: Cesium.BlendingState.ALPHA_BLEND,depthMask: false, // 關鍵:禁止寫入深度緩沖區}}),asynchronous: false});}/*** 添加圓類型GeometryInstance* @param {*} hexId * @param {*} radius * @param {*} color * @returns */createHexagonInstance_circle(hexId, radius = 5, color = "blue") {const coordinates = cellToLatLng(hexId, true); // 獲取六邊形邊界const center = Cesium.Cartographic.fromDegrees(coordinates[1], coordinates[0]);const _center = Cesium.Cartesian3.fromRadians(center.longitude,center.latitude, 0);return new Cesium.GeometryInstance({geometry: new Cesium.CircleOutlineGeometry({center: _center,radius: isNaN(radius) ? 5 : radius}),attributes: {color: Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.fromCssColorString(color))},id: hexId // 保存H3 ID以便交互});}/*** 添加線類型GeometryInstance* @returns */createHexagonInstance_line = (hexId, color = "#008df1a3") => {const coordinates = cellToBoundary(hexId, true); // 獲取六邊形邊界const positions = coordinates.map(coord => Cesium.Cartesian3.fromDegrees(coord[0], coord[1]));return new Cesium.GeometryInstance({geometry: new Cesium.PolylineGeometry({positions: positions,width : 2.0}),attributes: {color: Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.fromCssColorString(color))},id: hexId // 保存H3 ID以便交互});}/*** 添加多邊形類型GeometryInstance* @returns */createHexagonInstance_polygon(hexId, color = "#008df1a3") {const coordinates = cellToBoundary(hexId, true); // 獲取六邊形邊界const positions = coordinates.map(coord => Cesium.Cartesian3.fromDegrees(coord[0], coord[1], 200));return new Cesium.GeometryInstance({geometry: new Cesium.PolygonGeometry({polygonHierarchy: new Cesium.PolygonHierarchy(positions),height: 20 // 可選高度}),attributes: {color: Cesium.ColorGeometryInstanceAttribute.fromColor(Cesium.Color.fromCssColorString(color))},id: hexId // 保存H3 ID以便交互});}/*** 判斷當前選中的區域是否在全視口內* @param {*} positions * @returns * 0: 全不在視口中,不需要渲染* 1: 所選區域部分在視口內* 2: 所選區域全部在視口內*/isPolygonFullyInViewport(positions) {const viewRectangle = this.viewer.camera.computeViewRectangle();if (!viewRectangle || !positions || positions.length < 3) return 0;const _positions = positions.map(t => new Cesium.Cartographic(t[1],t[0]));let polygonRectangle = this.createValidRectangle(_positions);console.log(viewRectangle, polygonRectangle);// 所選區域全部在視口內if (_positions.every(t => Cesium.Rectangle.contains(viewRectangle, polygonRectangle))) {return 2;}// 所選區域全部不在視口內if (!_positions.some(t => Cesium.Rectangle.contains(viewRectangle, polygonRectangle))) {return 0;} else {// 所選區域部分在視口內return 1;}}// 輔助方法:創建有效的RectanglecreateValidRectangle(cartographics) {// 1. 計算邊界const west = Math.min(...cartographics.map(c => c.longitude));const south = Math.min(...cartographics.map(c => c.latitude));const east = Math.max(...cartographics.map(c => c.longitude));const north = Math.max(...cartographics.map(c => c.latitude));// 2. 驗證范圍if (west >= east || south >= north) {console.warn("無效的坐標范圍:", {west, south, east, north});return null;}// 3. 返回有效的Rectanglereturn Cesium.Rectangle.fromDegrees(west, south, east, north);}/*** 重置層級* @param {*} level 限制 最大是6 最小是3*/setLevel(level) {let _level= Math.max(Math.min(level, 6), 3);if (Object.is(this.config.level, _level)) return false;this.config.level = _level;return this.config.isPolygonFullDraw ? this.drawPolygonByViewFull() : this.render();;}/*** 重新選擇生成網格碼的區域* @param {*} position */setPolygonArea(position) {this.config.polygonArea = position;this.config.isPolygonFullDraw ? this.drawPolygonByViewFull() : this.render();}/*** 刪除標簽*/removeLabelCollection() {this.labelsCollection && this.labelsCollection.removeAll();}/*** 刪除可視化圖元*/clearOldPrimitive() {this.remove_select_primitive();this.PrimitiveMain && this.viewer.scene.primitives.remove(this.PrimitiveMain);this._circles.forEach(item => this.viewer.scene.primitives.remove(item));this.removeLabelCollection();}/*** 銷毀示例*/destory() {this.clearOldPrimitive();}
}
如何我們不是vue等框架項目,不容易用import和export方法來導出、引入的話,我們可以script標簽方式引入,然后在所有api前添加h3.就行了(如:h3.cellToLatLng(xxx)即可)。最后將export去掉,底部添加window.useHexagonTrellisCode = useHexagonTrellisCode;代碼就可以了。
最后想法
如果哪位同賽道的同事發現問題或者有更好的建議可以私信我,一起努力哦,
最后!!!
啥也不想說了,睡覺...
明天還要出差呢,牛馬的每一天(_ _)( - . - )(~O~)……( - . - )