diff --git a/CHANGELOG.md b/CHANGELOG.md index fb0ab69..cbcc864 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ## main ### ✨ Features and improvements +- Add `geoJSONToTile` function to generate a single tile directly from GeoJSON without building the full tile index. (by [montzkie18](https://github.com/montzkie18) and [lucaswoj](https://github.com/lucaswoj)). - _...Add new stuff here..._ ### 🐞 Bug fixes diff --git a/src/definitions.ts b/src/definitions.ts index 538685c..c0383a4 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -55,6 +55,18 @@ export type GeoJSONVTOptions = { debug?: number; }; +export type GeoJSONToTileOptions = GeoJSONVTOptions & { + /** + * Whether to wrap features around the antimeridian + * @default false + */ + wrap?: boolean; + /** + * Whether to clip features to the tile boundary + * @default false + */ + clip?: boolean; +}; export type StartEndSizeArray = number[] & { start?: number; end?: number; size?: number }; diff --git a/src/geoJSONToTile.ts b/src/geoJSONToTile.ts new file mode 100644 index 0000000..4552c37 --- /dev/null +++ b/src/geoJSONToTile.ts @@ -0,0 +1,58 @@ +import {clip} from './clip'; +import {convert} from './convert'; +import type {GeoJSONToTileOptions} from './definitions'; +import {defaultOptions} from './geojsonvt'; +import {createTile} from './tile'; +import {transformTile, type GeoJSONVTTile} from './transform'; +import {wrap} from './wrap'; + +/** + * Converts GeoJSON data directly to a single vector tile without building a tile index. + * + * Unlike the {@link GeoJSONVT} class which builds a hierarchical tile index for efficient + * repeated tile access, this function generates a single tile on-demand. This is useful when: + * - You only need one specific tile and don't need to query multiple tiles + * - The source data is already spatially filtered to the tile's bounding box + * - You want to avoid the overhead of building a full tile index + * + * @example + * ```ts + * import {geoJSONToTile} from '@maplibre/geojson-vt'; + * + * const geojson = { + * type: 'FeatureCollection', + * features: [{ + * type: 'Feature', + * geometry: { type: 'Point', coordinates: [-77.03, 38.90] }, + * properties: { name: 'Washington, D.C.' } + * }] + * }; + * + * const tile = geoJSONToTile(geojson, 10, 292, 391, { extent: 4096 }); + * ``` + * + * @param data - GeoJSON data (Feature, FeatureCollection, or Geometry) + * @param z - Tile zoom level + * @param x - Tile x coordinate + * @param y - Tile y coordinate + * @param options - Optional configuration for tile generation + * @returns The generated tile with geometries in tile coordinates, or null if no features + */ + +export function geoJSONToTile(data: GeoJSON.GeoJSON, z: number, x: number, y: number, options: GeoJSONToTileOptions = {}): GeoJSONVTTile { + options = {...defaultOptions, ...options}; + const {wrap: shouldWrap = false, clip: shouldClip = false} = options; + + let features = convert(data, options); + if (shouldWrap) { + features = wrap(features, options); + } + if (shouldClip || options.lineMetrics) { + const pow2 = 1 << z; + const buffer = options.buffer / options.extent; + const left = clip(features, pow2, (x - buffer), (x + 1 + buffer), 0, -1, 2, options); + features = clip(left || [], pow2, (y - buffer), (y + 1 + buffer), 1, -1, 2, options); + } + + return transformTile(createTile(features ?? [], z, x, y, options), options.extent); +} diff --git a/src/geojsonvt.ts b/src/geojsonvt.ts index c01a2b1..91722e5 100644 --- a/src/geojsonvt.ts +++ b/src/geojsonvt.ts @@ -6,7 +6,7 @@ import {createTile, type GeoJSONVTInternalTile} from './tile'; import {applySourceDiff, type GeoJSONVTSourceDiff} from './difference'; import type { GeoJSONVTInternalFeature, GeoJSONVTOptions } from './definitions'; -const defaultOptions: GeoJSONVTOptions = { +export const defaultOptions: GeoJSONVTOptions = { maxZoom: 14, indexMaxZoom: 5, indexMaxPoints: 100000, diff --git a/src/index.ts b/src/index.ts index fb45991..d74d16c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,16 +2,19 @@ import type {GeoJSONVTFeature, GeoJSONVTFeatureNonPoint, GeoJSONVTFeaturePoint, GeoJSONVTTile} from './transform'; import type {GeoJSONVTInternalTile, GeoJSONVTInternalTileFeature, GeoJSONVTInternalTileFeaturePoint, GeoJSONVTInternalTileFeatureNonPoint} from './tile'; import type {GeoJSONVTFeatureDiff, GeoJSONVTSourceDiff} from './difference'; -import type {GeoJSONVTInternalFeature, GeoJSONVTInternalLineStringFeature, GeoJSONVTInternalMultiLineStringFeature, GeoJSONVTInternalMultiPointFeature, GeoJSONVTInternalMultiPolygonFeature, GeoJSONVTInternalPointFeature, GeoJSONVTInternalPolygonFeature, GeoJSONVTOptions, PartialGeoJSONVTFeature, StartEndSizeArray} from './definitions'; +import type {GeoJSONVTInternalFeature, GeoJSONVTInternalLineStringFeature, GeoJSONVTInternalMultiLineStringFeature, GeoJSONVTInternalMultiPointFeature, GeoJSONVTInternalMultiPolygonFeature, GeoJSONVTInternalPointFeature, GeoJSONVTInternalPolygonFeature, GeoJSONVTOptions, GeoJSONToTileOptions, PartialGeoJSONVTFeature, StartEndSizeArray} from './definitions'; import {GeoJSONVT} from './geojsonvt'; export default function geojsonvt(data: GeoJSON.GeoJSON, options?: GeoJSONVTOptions) { return new GeoJSONVT(data, options); } -export type { - GeoJSONVTInternalFeature, - GeoJSONVTOptions, +export {geoJSONToTile} from './geoJSONToTile'; + +export type { + GeoJSONVTInternalFeature, + GeoJSONVTOptions, + GeoJSONToTileOptions, GeoJSONVTInternalTile, GeoJSONVTInternalTileFeature, PartialGeoJSONVTFeature, diff --git a/test/fixtures/single-tile.json b/test/fixtures/single-tile.json new file mode 100644 index 0000000..6e846d1 --- /dev/null +++ b/test/fixtures/single-tile.json @@ -0,0 +1,707 @@ +{ + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -77.066104, + 38.910203 + ], + [ + -77.066106, + 38.910321 + ], + [ + -77.066112, + 38.910758 + ], + [ + -77.065249, + 38.910775 + ], + [ + -77.065178, + 38.910793 + ], + [ + -77.065139, + 38.910804 + ], + [ + -77.064904, + 38.91036 + ], + [ + -77.064855, + 38.910268 + ], + [ + -77.064621, + 38.909825 + ], + [ + -77.06459, + 38.909766 + ], + [ + -77.064342, + 38.909298 + ], + [ + -77.064281, + 38.909182 + ], + [ + -77.064201, + 38.909226 + ], + [ + -77.064175, + 38.909235 + ], + [ + -77.064149, + 38.909241 + ], + [ + -77.064122, + 38.909244 + ], + [ + -77.06336, + 38.90926 + ], + [ + -77.061442, + 38.909288 + ], + [ + -77.060801, + 38.909297 + ], + [ + -77.059237, + 38.909315 + ], + [ + -77.058186, + 38.90933 + ], + [ + -77.057113, + 38.909343 + ], + [ + -77.055623, + 38.909368 + ], + [ + -77.054762, + 38.909377 + ], + [ + -77.053951, + 38.909389 + ], + [ + -77.053904, + 38.909393 + ], + [ + -77.053858, + 38.909399 + ], + [ + -77.053813, + 38.909408 + ], + [ + -77.052833, + 38.909635 + ], + [ + -77.052799, + 38.909642 + ], + [ + -77.052772, + 38.909645 + ], + [ + -77.052692, + 38.90965 + ], + [ + -77.052443, + 38.909649 + ], + [ + -77.05096, + 38.909639 + ], + [ + -77.050327, + 38.909634 + ], + [ + -77.049545, + 38.909631 + ], + [ + -77.049533, + 38.909635 + ], + [ + -77.049341, + 38.909635 + ], + [ + -77.048903, + 38.909635 + ], + [ + -77.048797, + 38.909634 + ], + [ + -77.04773, + 38.909639 + ], + [ + -77.046632, + 38.90964 + ], + [ + -77.045758, + 38.909641 + ], + [ + -77.044877, + 38.909643 + ], + [ + -77.044578, + 38.909641 + ], + [ + -77.044493, + 38.909635 + ], + [ + -77.04442, + 38.909626 + ], + [ + -77.044345, + 38.909608 + ], + [ + -77.044283, + 38.909578 + ], + [ + -77.044249, + 38.909541 + ], + [ + -77.044212, + 38.909496 + ], + [ + -77.044199, + 38.909458 + ], + [ + -77.044182, + 38.909421 + ], + [ + -77.044162, + 38.909385 + ], + [ + -77.04414, + 38.90935 + ], + [ + -77.044113, + 38.909313 + ], + [ + -77.044083, + 38.90928 + ], + [ + -77.044051, + 38.909247 + ], + [ + -77.044018, + 38.909165 + ], + [ + -77.044018, + 38.909117 + ], + [ + -77.044029, + 38.909071 + ], + [ + -77.044037, + 38.909038 + ], + [ + -77.044058, + 38.908981 + ], + [ + -77.043996, + 38.909021 + ], + [ + -77.043919, + 38.909052 + ], + [ + -77.043854, + 38.909067 + ], + [ + -77.043798, + 38.909073 + ], + [ + -77.043755, + 38.909075 + ], + [ + -77.043689, + 38.909056 + ], + [ + -77.043656, + 38.909048 + ], + [ + -77.04359, + 38.909036 + ], + [ + -77.043527, + 38.909028 + ], + [ + -77.043486, + 38.909026 + ], + [ + -77.043444, + 38.909025 + ], + [ + -77.043402, + 38.909026 + ], + [ + -77.043344, + 38.909029 + ], + [ + -77.043286, + 38.909037 + ], + [ + -77.043263, + 38.909041 + ], + [ + -77.04323, + 38.909047 + ], + [ + -77.043194, + 38.909056 + ], + [ + -77.043022, + 38.909118 + ], + [ + -77.042995, + 38.909132 + ], + [ + -77.04296, + 38.909151 + ], + [ + -77.042938, + 38.909165 + ], + [ + -77.042915, + 38.90918 + ], + [ + -77.042875, + 38.909209 + ], + [ + -77.04284, + 38.909239 + ], + [ + -77.042823, + 38.909255 + ], + [ + -77.042791, + 38.909288 + ], + [ + -77.042774, + 38.909307 + ], + [ + -77.042712, + 38.909315 + ], + [ + -77.042637, + 38.909322 + ], + [ + -77.042551, + 38.909317 + ], + [ + -77.042422, + 38.909299 + ], + [ + -77.041676, + 38.90903 + ], + [ + -77.039935, + 38.908425 + ], + [ + -77.038503, + 38.907925 + ], + [ + -77.037656, + 38.907632 + ], + [ + -77.037477, + 38.907534 + ], + [ + -77.037317, + 38.907438 + ], + [ + -77.037238, + 38.90739 + ], + [ + -77.037121, + 38.907315 + ], + [ + -77.036997, + 38.907233 + ], + [ + -77.036875, + 38.907137 + ], + [ + -77.036833, + 38.907094 + ], + [ + -77.036817, + 38.907081 + ], + [ + -77.036795, + 38.907068 + ], + [ + -77.036774, + 38.907058 + ], + [ + -77.036767, + 38.907052 + ], + [ + -77.036712, + 38.907034 + ], + [ + -77.03669, + 38.907028 + ], + [ + -77.036643, + 38.907022 + ], + [ + -77.03661, + 38.907021 + ], + [ + -77.03653, + 38.907021 + ], + [ + -77.036474, + 38.907021 + ], + [ + -77.03643, + 38.907025 + ], + [ + -77.03639, + 38.907033 + ], + [ + -77.036295, + 38.907058 + ], + [ + -77.036005, + 38.906995 + ], + [ + -77.035439, + 38.906869 + ], + [ + -77.035142, + 38.906757 + ], + [ + -77.035049, + 38.906725 + ], + [ + -77.0348, + 38.906641 + ], + [ + -77.034673, + 38.906594 + ], + [ + -77.034568, + 38.906558 + ], + [ + -77.034059, + 38.906382 + ], + [ + -77.033931, + 38.906338 + ], + [ + -77.032623, + 38.905883 + ], + [ + -77.03129, + 38.905426 + ], + [ + -77.030031, + 38.904982 + ], + [ + -77.029623, + 38.904835 + ], + [ + -77.028082, + 38.904304 + ], + [ + -77.027679, + 38.904167 + ], + [ + -77.02705, + 38.903949 + ], + [ + -77.026409, + 38.903729 + ], + [ + -77.025984, + 38.903579 + ], + [ + -77.024321, + 38.902997 + ], + [ + -77.024139, + 38.902954 + ], + [ + -77.023952, + 38.902954 + ], + [ + -77.023188, + 38.902956 + ], + [ + -77.022699, + 38.902958 + ], + [ + -77.021917, + 38.90296 + ], + [ + -77.021918, + 38.902902 + ], + [ + -77.021918, + 38.902521 + ], + [ + -77.019907, + 38.902521 + ], + [ + -77.018928, + 38.902521 + ], + [ + -77.016176, + 38.902519 + ], + [ + -77.015177, + 38.902518 + ], + [ + -77.013722, + 38.902514 + ], + [ + -77.012171, + 38.902516 + ], + [ + -77.011237, + 38.902516 + ], + [ + -77.009846, + 38.902515 + ], + [ + -77.009117, + 38.902514 + ], + [ + -77.009126, + 38.901346 + ], + [ + -77.00912, + 38.900203 + ], + [ + -77.009062, + 38.900203 + ], + [ + -77.008975, + 38.900203 + ], + [ + -77.008004, + 38.900198 + ], + [ + -77.00631, + 38.900193 + ], + [ + -77.005531, + 38.900197 + ], + [ + -77.002929, + 38.9002 + ], + [ + -77.002038, + 38.900203 + ], + [ + -77.001892, + 38.900203 + ], + [ + -77.000571, + 38.900205 + ], + [ + -76.999507, + 38.900204 + ], + [ + -76.998442, + 38.900204 + ], + [ + -76.998369, + 38.900204 + ], + [ + -76.996167, + 38.900205 + ], + [ + -76.994961, + 38.900203 + ], + [ + -76.994962, + 38.899748 + ], + [ + -76.994961, + 38.899626 + ], + [ + -76.994961, + 38.899367 + ], + [ + -76.994961, + 38.898908 + ], + [ + -76.994962, + 38.898447 + ] + ] + }, + "properties": { + "name": "P Street Northwest - Massachusetts Avenue Northwest" + } +} diff --git a/test/geoJSONToTile.test.ts b/test/geoJSONToTile.test.ts new file mode 100644 index 0000000..a48017e --- /dev/null +++ b/test/geoJSONToTile.test.ts @@ -0,0 +1,92 @@ + +import {test, expect} from 'vitest'; +import fs from 'fs'; +import {geoJSONToTile} from '../src/geoJSONToTile'; + +const square = [{ + geometry: [[[4160, -64], [4160, 4160], [-64, 4160], [-64, -64], [4160, -64]]], + type: 3, + tags: {name: 'Pennsylvania', density: 284.3}, + id: '42' +}]; + +test('geoJSONToTile: converts all geometries to a single vector tile', () => { + const geojson = getJSON('single-tile.json'); + const tile = geoJSONToTile(geojson, 12, 1171, 1566); + + expect(tile.features.length).toBe(1); + expect(tile.features[0].tags.name).toBe('P Street Northwest - Massachusetts Avenue Northwest'); +}); + +test('geoJSONToTile: wrap features across the antimeridian', () => { + const geojson: GeoJSON.FeatureCollection = { + type: 'FeatureCollection', + features: [{ + type: 'Feature', + properties: {name: 'test'}, + geometry: { + type: 'LineString', + coordinates: [[-200, 0], [200, 0]] + } + }] + }; + + const tileWithoutWrap = geoJSONToTile(geojson, 0, 0, 0, {wrap: false, clip: true}); + expect(tileWithoutWrap.features).toEqual([{ + type: 2, + tags: {name: 'test'}, + geometry: [[[-64, 2048], [4160, 2048]]] + }]); + + const tileWithWrap = geoJSONToTile(geojson, 0, 0, 0, {wrap: true, clip: true}); + expect(tileWithWrap.features).toEqual([ + {type: 2, tags: {name: 'test'}, geometry: [[[3868, 2048], [4160, 2048]]]}, + {type: 2, tags: {name: 'test'}, geometry: [[[-64, 2048], [4160, 2048]]]}, + {type: 2, tags: {name: 'test'}, geometry: [[[-64, 2048], [228, 2048]]]} + ]); +}); + +test('geoJSONToTile: shouldWrap duplicates features that extend beyond world bounds', () => { + const geojson: GeoJSON.FeatureCollection = { + type: 'FeatureCollection', + features: [{ + type: 'Feature', + properties: {name: 'test'}, + geometry: { + type: 'LineString', + coordinates: [[-200, 0], [-170, 0]] + } + }] + }; + + const tileWithoutWrap = geoJSONToTile(geojson, 0, 0, 0, {wrap: false, clip: false}); + expect(tileWithoutWrap.features).toEqual([{ + type: 2, + tags: {name: 'test'}, + geometry: [[[-228, 2048], [114, 2048]]] + }]); + + const tileWithWrap = geoJSONToTile(geojson, 0, 0, 0, {wrap: true, clip: false}); + expect(tileWithWrap.features).toEqual([ + {type: 2, tags: {name: 'test'}, geometry: [[[3868, 2048], [4160, 2048]]]}, + {type: 2, tags: {name: 'test'}, geometry: [[[-64, 2048], [114, 2048]]]} + ]); +}); + +test('geoJSONToTile: clips geometries outside the tile', () => { + const geojson = getJSON('us-states.json'); + + const tile1 = geoJSONToTile(geojson, 7, 37, 48, {clip: true}); + expect(tile1.features).toEqual(getJSON('us-states-z7-37-48.json')); + + const tile2 = geoJSONToTile(geojson, 9, 148, 192, {clip: true}); + expect(tile2.features).toEqual(square); + + expect(geoJSONToTile(geojson, 11, 800, 400, {clip: true}).features).toEqual([]); + expect(geoJSONToTile(geojson, -5, 123.25, 400.25, {clip: true}).features).toEqual([]); + expect(geoJSONToTile(geojson, 25, 200, 200, {clip: true}).features).toEqual([]); +}); + +function getJSON(name: string) { + return JSON.parse(fs.readFileSync(new URL(`fixtures/${name}`, import.meta.url), 'utf-8')); +}