@loaders.gl/mvt
Version:
Loader for Mapbox Vector Tiles
230 lines (206 loc) • 6.84 kB
text/typescript
// 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 type {MVTTile} from '../mvt-pbf/mvt-types';
import {writeMVT} from '../mvt-pbf/write-mvt-to-pbf';
import GeoJSONWrapper from './geojson-wrapper';
import type {GeoJSON, FeatureCollection, Geometry} from '@loaders.gl/schema';
import {copyToArrayBuffer} from '@loaders.gl/loader-utils';
export type FromGeojsonOptions = {
layerName?: string;
version?: number;
extent?: number;
tileIndex?: {x: number; y: number; z: number};
};
/**
* 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: FeatureCollection, options: FromGeojsonOptions): ArrayBuffer {
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 as any).name = options.layerName || 'geojsonLayer';
(layer as any).version = options.version || 1;
(layer as any).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: MVTTile): ArrayBuffer {
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: GeoJSON): FeatureCollection {
// 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: number,
tileIndex?: {x: number; y: number; z: number}
) {
if (features.every(isVectorTileFeature)) {
return features;
}
return features.map((feature) => convertFeatureToVectorTile(feature, extent, tileIndex));
}
function convertFeatureToVectorTile(
feature,
extent: number,
tileIndex?: {x: number; y: number; z: number}
) {
const geometry = feature.geometry as 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: Geometry,
extent: number,
tileIndex?: {x: number; y: number; z: number}
) {
switch (geometry.type) {
case 'Point':
return [projectPointToTile(geometry.coordinates as number[], extent, tileIndex)];
case 'MultiPoint':
return geometry.coordinates.map((coord) =>
projectPointToTile(coord as number[], extent, tileIndex)
);
case 'LineString':
return [
geometry.coordinates.map((coord) =>
projectPointToTile(coord as number[], extent, tileIndex)
)
];
case 'MultiLineString':
return geometry.coordinates.map((line) =>
line.map((coord) => projectPointToTile(coord as number[], extent, tileIndex))
);
case 'Polygon':
return geometry.coordinates.map((ring) =>
ring.map((coord) => projectPointToTile(coord as number[], extent, tileIndex))
);
case 'MultiPolygon':
return geometry.coordinates.flatMap((polygon) =>
polygon.map((ring) =>
ring.map((coord) => projectPointToTile(coord as number[], extent, tileIndex))
)
);
default:
throw new Error(`Unsupported geometry type: ${geometry.type}`);
}
}
function projectPointToTile(
point: number[],
extent: number,
tileIndex?: {x: number; y: number; z: number}
) {
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: number[]) {
return Math.abs(point[0]) <= 1 && Math.abs(point[1]) <= 1;
}
function isLngLatPoint(point: number[]) {
return Math.abs(point[0]) <= 180 && Math.abs(point[1]) <= 90;
}
function projectLngLatToTile(
point: number[],
tileIndex: {x: number; y: number; z: number},
extent: number
) {
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): boolean {
return typeof feature?.type === 'number' && Array.isArray(feature.geometry);
}
function getVectorTileType(type: Geometry['type']): 1 | 2 | 3 {
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}`);
}
}