@loaders.gl/mvt
Version:
Loader for Mapbox Vector Tiles
162 lines • 6.23 kB
JavaScript
// loaders.gl
// SPDX-License-Identifier: MIT
// Copyright vis.gl contributors
// Forked from https://github.com/mapbox/vt-pbf under MIT License Copyright (c) 2015 Anand Thakker
import Pbf from 'pbf';
import { writeMVT } from "../mvt-pbf/write-mvt-to-pbf.js";
import GeoJSONWrapper from "./geojson-wrapper.js";
import { copyToArrayBuffer } from '@loaders.gl/loader-utils';
/**
* Serialize a map of geojson layers
* loaders.gl addition
*
* @param geojson
* @param [options] - An object specifying the vector-tile specification version and extent that were used to create `layers`.
* @param [options.extent=4096] - Extent of the vector tile
* @return uncompressed, pbf-serialized tile data
*/
export function fromGeojson(geojson, options) {
options = options || {};
geojson = normalizeGeojson(geojson);
const extent = options.extent || 4096;
const features = convertFeaturesToVectorTileFeatures(geojson.features, extent, options.tileIndex);
const layer = new GeoJSONWrapper(features, { ...options, extent });
// TODO - this is broken
layer.name = options.layerName || 'geojsonLayer';
layer.version = options.version || 1;
layer.extent = options.extent || 4096;
// @ts-expect-error
return fromVectorTileJs({ layers: { [layer.name]: layer } });
}
/**
* Serialize a vector-tile-js-created tile to pbf
*
* @param tile
* @return uncompressed, pbf-serialized tile data
*/
export function fromVectorTileJs(tile) {
const pbf = new Pbf();
writeMVT(tile, pbf);
const uint8Array = pbf.finish();
// TODO - make sure no byteOffsets/byteLenghts are used?
return copyToArrayBuffer(uint8Array.buffer, uint8Array.byteOffset, uint8Array.byteOffset + uint8Array.byteLength);
}
/**
* Serialized a geojson-vt-created tile to pbf.
*
* @param vtLayers - An object mapping layer names to geojson-vt-created vector tile objects
* @param [options] - An object specifying the vector-tile specification version and extent that were used to create `layers`.
* @param [options.version=1] - Version of vector-tile spec used
* @param [options.extent=4096] - Extent of the vector tile
* @return uncompressed, pbf-serialized tile data
*
export function fromGeojsonVt(vtLayers, options): ArrayBuffer {
options = options || {};
const layers = {};
for (const key in vtLayers) {
layers[key] = new GeoJSONWrapper(vtLayers[key].features, options);
layers[key].name = key;
layers[key].version = options.version;
layers[key].extent = options.extent;
}
return fromVectorTileJs({layers});
}
*/
export function normalizeGeojson(geojson) {
// Array of features
if (Array.isArray(geojson)) {
return {
type: 'FeatureCollection',
features: geojson
};
}
// A single feature
if (geojson.type === 'Feature') {
return {
type: 'FeatureCollection',
features: [geojson]
};
}
throw new Error('Invalid GeoJSON object');
}
function convertFeaturesToVectorTileFeatures(features, extent, tileIndex) {
if (features.every(isVectorTileFeature)) {
return features;
}
return features.map((feature) => convertFeatureToVectorTile(feature, extent, tileIndex));
}
function convertFeatureToVectorTile(feature, extent, tileIndex) {
const geometry = feature.geometry;
const type = getVectorTileType(geometry.type);
return {
id: typeof feature.id === 'number' ? feature.id : undefined,
type,
geometry: projectGeometryToTileSpace(geometry, extent, tileIndex),
tags: feature.properties || {}
};
}
function projectGeometryToTileSpace(geometry, extent, tileIndex) {
switch (geometry.type) {
case 'Point':
return [projectPointToTile(geometry.coordinates, extent, tileIndex)];
case 'MultiPoint':
return geometry.coordinates.map((coord) => projectPointToTile(coord, extent, tileIndex));
case 'LineString':
return [
geometry.coordinates.map((coord) => projectPointToTile(coord, extent, tileIndex))
];
case 'MultiLineString':
return geometry.coordinates.map((line) => line.map((coord) => projectPointToTile(coord, extent, tileIndex)));
case 'Polygon':
return geometry.coordinates.map((ring) => ring.map((coord) => projectPointToTile(coord, extent, tileIndex)));
case 'MultiPolygon':
return geometry.coordinates.flatMap((polygon) => polygon.map((ring) => ring.map((coord) => projectPointToTile(coord, extent, tileIndex))));
default:
throw new Error(`Unsupported geometry type: ${geometry.type}`);
}
}
function projectPointToTile(point, extent, tileIndex) {
if (isNormalizedPoint(point)) {
return [Math.round(point[0] * extent), Math.round(point[1] * extent)];
}
if (tileIndex && isLngLatPoint(point)) {
return projectLngLatToTile(point, tileIndex, extent);
}
return [Math.round(point[0]), Math.round(point[1])];
}
function isNormalizedPoint(point) {
return Math.abs(point[0]) <= 1 && Math.abs(point[1]) <= 1;
}
function isLngLatPoint(point) {
return Math.abs(point[0]) <= 180 && Math.abs(point[1]) <= 90;
}
function projectLngLatToTile(point, tileIndex, extent) {
const [lng, lat] = point;
const { x, y, z } = tileIndex;
const size = extent * Math.pow(2, z);
const x0 = extent * x;
const y0 = extent * y;
const worldX = ((lng + 180) / 360) * size;
const worldY = ((180 - (180 / Math.PI) * Math.log(Math.tan(Math.PI / 4 + (lat * Math.PI) / 180 / 2))) * size) /
360;
return [Math.round(worldX - x0), Math.round(worldY - y0)];
}
function isVectorTileFeature(feature) {
return typeof feature?.type === 'number' && Array.isArray(feature.geometry);
}
function getVectorTileType(type) {
switch (type) {
case 'Point':
case 'MultiPoint':
return 1;
case 'LineString':
case 'MultiLineString':
return 2;
case 'Polygon':
case 'MultiPolygon':
return 3;
default:
throw new Error(`Unknown geometry type: ${type}`);
}
}
//# sourceMappingURL=to-vector-tile.js.map