// 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}`); } }