vgrid-maplibre
Version:
DGGS Visualization for MapLibre GL JS
222 lines (194 loc) • 6.09 kB
JavaScript
import * as h3 from 'https://unpkg.com/h3-js/dist/h3-js.es.js';
class H3Grid {
constructor(map, options = {}) {
this.map = map;
this.latitudeMax = 90;
this.latitudeMin = -this.latitudeMax;
this.longitudeMax = 180;
this.longitudeMin = -this.longitudeMax;
this.extraFillArea = 0.5;
this.options = {
color: options.color || 'rgba(255, 0, 0, 1)',
width: options.width || 1,
redraw: options.redraw || 'move', // Default to redraw on move
};
this.sourceId = 'h3-grid';
this.gridLayerId = 'h3-grid-layer';
this.initialize();
}
initialize() {
if (!this.map.getSource(this.sourceId)) {
this.map.addSource(this.sourceId, {
type: 'geojson',
data: this.generateGrid(),
});
}
if (!this.map.getLayer(this.gridLayerId)) {
this.map.addLayer({
id: this.gridLayerId,
source: this.sourceId,
type: 'fill',
layout: {},
paint: {
'fill-color': 'transparent',
'fill-opacity': 1,
}
});
}
if (!this.map.getLayer('outline')) {
this.map.addLayer({
id: 'outline',
type: 'line',
source: this.sourceId,
layout: {},
paint: {
'line-color': ['get', 'color'],
'line-width': this.options.width,
}
});
}
if (!this._hasListener) {
this.map.on(this.options.redraw, () => this.updateGrid());
this._hasListener = true;
}
}
updateGrid() {
const newGrid = this.generateGrid();
const source = this.map.getSource(this.sourceId);
if (source) {
source.setData(newGrid);
}
}
show() {
if (!this.map.getLayer(this.gridLayerId)) {
this.map.addLayer({
id: this.gridLayerId,
source: this.sourceId,
type: 'fill',
layout: {},
paint: {
'fill-color': 'transparent',
'fill-opacity': 1,
},
});
}
if (!this.map.getLayer('outline')) {
this.map.addLayer({
id: 'outline',
type: 'line',
source: this.sourceId,
layout: {},
paint: {
'line-color': ['get', 'color'],
'line-width': this.options.width,
}
});
}
}
remove() {
if (this.map.getLayer(this.gridLayerId)) {
this.map.removeLayer(this.gridLayerId);
}
if (this.map.getLayer('outline')) {
this.map.removeLayer('outline');
}
}
// Determine the H3 resolution based on zoom level
getResolution(zoom) {
if (zoom <= 3.0) return 0;
if (zoom <= 4.4) return 1;
if (zoom <= 5.7) return 2;
if (zoom <= 7.1) return 3;
if (zoom <= 8.4) return 4;
if (zoom <= 9.8) return 5;
if (zoom <= 11.4) return 6;
if (zoom <= 12.7) return 7;
if (zoom <= 14.1) return 8;
if (zoom <= 15.5) return 9;
if (zoom <= 16.8) return 10;
if (zoom <= 18.2) return 11;
if (zoom <= 19.5) return 12;
if (zoom <= 21.1) return 13;
if (zoom <= 21.9) return 14;
return 15;
}
// Render hexagons based on current map zoom
generateGrid() {
var currentZoom = this.map.getZoom();
var h3res = this.getResolution(currentZoom);
const iw = window.innerWidth;
const ih = window.innerHeight;
const cUL = this.map.unproject([0, 0]).toArray(); // Upper left
const cLR = this.map.unproject([iw, ih]).toArray(); // Lower right
const x1 = Math.min(cUL[0], cLR[0]);
const x2 = Math.max(cUL[0], cLR[0]);
const y1 = Math.min(cUL[1], cLR[1]);
const y2 = Math.max(cUL[1], cLR[1]);
const dh = x2 - x1;
const dv = y2 - y1;
let x1withBuffer = x1 - dh * this.extraFillArea;
let x2withBuffer = x2 + dh * this.extraFillArea;
let y1withBuffer = y1 - dv * this.extraFillArea;
let y2withBuffer = y2 + dv * this.extraFillArea;
x1withBuffer = Math.max(x1withBuffer, this.longitudeMin);
x2withBuffer = Math.min(x2withBuffer, this.longitudeMax);
y1withBuffer = Math.max(y1withBuffer, this.latitudeMin);
y2withBuffer = Math.min(y2withBuffer, this.latitudeMax);
let coordinates = [];
const xIncrement = 180;
let lowerX = x1withBuffer;
while (lowerX < this.longitudeMax && lowerX < x2withBuffer) {
let upperX = Math.min(lowerX + xIncrement, x2withBuffer, 180);
coordinates.push([
[y2withBuffer, lowerX],
[y2withBuffer, upperX],
[y1withBuffer, upperX],
[y1withBuffer, lowerX]
]);
lowerX += xIncrement;
}
var shapes = [].concat(...coordinates.map(e => h3.polygonToCells(e, h3res)));
var features = [];
for (var i = 0; i < shapes.length; i++) {
let h3_id = shapes[i];
const exists = features.some(f => f.properties.h3_id === h3_id);
if (exists) continue;
let boundary = h3.cellToBoundary(h3_id, true);
// Adjust boundary coordinates if they cross the anti-meridian
if (boundary.find((e) => e[0] < -130) !== undefined) {
boundary = boundary.map((e) => e[0] > 0 ? [e[0] - 360, e[1]] : e);
}
const resolution = h3.getResolution(h3_id);
const edge_unit = resolution > 7 ? h3.UNITS.m : h3.UNITS.km;
const area_unit = resolution > 7 ? h3.UNITS.m2 : h3.UNITS.km2;
const icosa_faces = h3.getIcosahedronFaces(h3_id);
let area = h3.cellArea(h3_id, area_unit);
area = parseFloat(area.toFixed(1)).toLocaleString();
let num_hex = h3.getNumCells(h3res);
num_hex = num_hex.toLocaleString();
const feature = {
type: "Feature",
properties: {
color: h3.isPentagon(h3_id) ? 'cyan' : this.options.color,
h3_id: h3_id,
resolution: resolution,
icosa_faces: icosa_faces,
area: area,
area_unit: area_unit,
edge_unit: edge_unit,
num_hex: num_hex
},
geometry: {
type: "Polygon",
coordinates: [boundary]
}
};
features.push(feature);
}
return {
type: "FeatureCollection",
features,
};
}
}
export default H3Grid;