Skip to content

Add "geoJSONToTile" method - #38

Merged
wayofthefuture merged 25 commits into
maplibre:mainfrom
lucaswoj:geojson-to-tile
Jan 30, 2026
Merged

Add "geoJSONToTile" method#38
wayofthefuture merged 25 commits into
maplibre:mainfrom
lucaswoj:geojson-to-tile

Conversation

@lucaswoj

@lucaswoj lucaswoj commented Jan 29, 2026

Copy link
Copy Markdown
Contributor

This feature was added to geojson-vt-cpp at mapbox/geojson-vt-cpp#65

It was ported to geojson-vt by @montzkie18 at mapbox/geojson-vt#152 (related to mapbox/geojson-vt#92 ) but has not been merged by the project maintainers there.

The demand for this feature seems to be reasonably high and the cost of adding it very low. So I'm humbly proposing that we merge it into this maplibre/geojson-vt fork.

Original PR comments:

Add #92 - Generate exact single tile

Exposing new method geoJSONToTile to generate a specific VT based on passed z/x/y values.

This should skip generating parent tiles on all higher zoom levels for the requested tile.

This ports the C++ functionality implemented by @asheemmamoowala in mapbox/geojson-vt-cpp#65

Comment thread src/geojsonvt.ts Outdated
Comment thread src/geojsonvt.ts Outdated
Comment thread src/geojsonvt.ts Outdated
features = clip(left || [], pow2, (y - buffer), (y + 1 + buffer), 1, -1, 2, options);
}

return features

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would expect an empty array instead of null, can features really be null?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an empty array as a return value is a better API I believe.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function returns a tile, not an array. I have just pushed 57ecfc5 which returns an empty tile instead of null.

Comment thread src/geojsonvt.ts Outdated
Comment thread CHANGELOG.md
@codecov-commenter

codecov-commenter commented Jan 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (b3cf995) to head (57ecfc5).

Additional details and impacted files
@@            Coverage Diff            @@
##              main       #38   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           10        11    +1     
  Lines          752       764   +12     
  Branches       193       199    +6     
=========================================
+ Hits           752       764   +12     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@HarelM

HarelM commented Jan 29, 2026

Copy link
Copy Markdown
Collaborator

Thanks for taking the time to open this PR!

Looking at the cod, the tests and the coverage report, I'm guessing there's some cases missing in terms of testing...

[Optional] I would also consider moving this method to a different file as it is not really part of the geojsonvt class...

@wayofthefuture

Copy link
Copy Markdown
Collaborator

I spent quite a bit of time looking into this and thinking it through. While this approach may be beneficial in some cases, it likely won’t help with larger datasets.

The performance benefit of geojson-vt comes from ascending the tile pyramid until it reaches geometry that has already been processed, and then only handling a small subset at deeper zoom levels. Its clipping is also incremental - it occurs after traversing upward to the deepest clipped tile and then reclipping back down.

In contrast, geoJSONToTile processes the entire geometry on every tile request, which removes most of those performance advantages.

I’m fine with adding this to the package, but I'd strongly prefer that the function and its options live in a separate file and be exported as a completely independent class.

@lucaswoj

Copy link
Copy Markdown
Contributor Author

I would also consider moving this method to a different file as it is not really part of the geojsonvt class...

Done in c96b8a3

@wayofthefuture wayofthefuture left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job

@lucaswoj

Copy link
Copy Markdown
Contributor Author

Looking at the cod, the tests and the coverage report, I'm guessing there's some cases missing in terms of testing...

I just added a couple more test cases. src/geoJSONToTile.ts has full coverage now.

@lucaswoj

Copy link
Copy Markdown
Contributor Author

I spent quite a bit of time looking into this and thinking it through. While this approach may be beneficial in some cases, it likely won’t help with larger datasets.

The performance benefit of geojson-vt comes from ascending the tile pyramid until it reaches geometry that has already been processed, and then only handling a small subset at deeper zoom levels. Its clipping is also incremental - it occurs after traversing upward to the deepest clipped tile and then reclipping back down.

In contrast, geoJSONToTile processes the entire geometry on every tile request, which removes most of those performance advantages.

I’m fine with adding this to the package, but I'd strongly prefer that the function and its options live in a separate file and be exported as a completely independent class.

I agree with you that this won't lead to any maplibre-gl-js perf improvements. This function serves other users.

@lucaswoj
lucaswoj requested a review from HarelM January 29, 2026 21:38
Comment thread src/geoJSONToTile.ts Outdated
@HarelM

HarelM commented Jan 30, 2026

Copy link
Copy Markdown
Collaborator

Added a few last comments, looks good otherwise.

@lucaswoj
lucaswoj requested a review from HarelM January 30, 2026 15:30
@wayofthefuture
wayofthefuture merged commit 3733f98 into maplibre:main Jan 30, 2026
1 check passed
@lucaswoj
lucaswoj deleted the geojson-to-tile branch January 30, 2026 17:04
@wayofthefuture

Copy link
Copy Markdown
Collaborator

@lucaswoj It would be interesting to know which cases this is actually more performant. If it applies to many cases, or especially animation cases, then it may be worth adding an option into MapLibre to allow for the use of this method.

@ingalls

ingalls commented Jan 30, 2026

Copy link
Copy Markdown

@wayofthefuture We use this function extensively for creating vector tiles dynamically from a vector provider that doesn't support vector tile generation on the server. IE the user provides a vector tile source and then we provide a traditional ZXY endpoint that dynamically creates tiles from the provided data source. In this case the server only needs a single tile and not the entire pyramid.

@wayofthefuture

Copy link
Copy Markdown
Collaborator

dynamically creates tiles from the provided data source

tiles plural? The entire pyramid is not generated - only branches on the fly - and indexMaxZoom can further limit pre-generation. So you may notice this method is heavier...

@wayofthefuture

wayofthefuture commented Jan 30, 2026

Copy link
Copy Markdown
Collaborator

@ingalls, @lucaswoj Not sure if this helps I'm sure my benchmarks need a little adjustment... see the PR here: #40

1k features, z14:
  geojsonvt.getTile (indexed): 1284854.57 ops/sec
  geojsonvt.getTile (non-indexed): 3042.81 ops/sec
  geoJSONToTile (on-demand): 1391.53 ops/sec

100k features, z14:
  geojsonvt.getTile (indexed): 1609136.93 ops/sec
  geojsonvt.getTile (non-indexed): 7.63 ops/sec
  geoJSONToTile (on-demand): 5.30 ops/sec

@HarelM

HarelM commented Jan 30, 2026

Copy link
Copy Markdown
Collaborator

How is it possible that noon indexed is faster than to tile? Caching?

@wayofthefuture

wayofthefuture commented Jan 30, 2026

Copy link
Copy Markdown
Collaborator

How is it possible that noon indexed is faster than to tile? Caching?

Because the test is requesting 4 tiles to simulate 1 map view load, meaning that it re-parses the full geometry on each tile. In reality it would likely be closer to 16 tiles, or:

1k features, z14:
  geojsonvt.getTile (indexed): 285208.18 ops/sec
  geojsonvt.getTile (non-indexed): 2528.44 ops/sec
  geoJSONToTile (on-demand): 329.98 ops/sec

100k features, z14:
  geojsonvt.getTile (indexed): 372989.98 ops/sec
  geojsonvt.getTile (non-indexed): 7.18 ops/sec
  geoJSONToTile (on-demand): 1.38 ops/sec

@jgimbel

jgimbel commented Feb 23, 2026

Copy link
Copy Markdown

Thanks all for adding this function.

It looks like this PR was merged, but the code was never pushed to NPM. Is there any more work that I can help with so you can release this to NPM?

This would help with a use case I currently have where I am attempting to wrap an external JSON API as a MVT API. It would help reduce having to build multiple tiles, and allow me to do this:

  async mvt(z: number, x: number, y: number, options: PlacesSearchOptions): Promise<Uint8Array> {
    // Get 100 points in this tile
    const places = await this.placesInTile(z, x, y, options)

    // Shortcut if no places were found
    if (!places.length <= 0) {
      return new Uint8Array()
    }

    // Convert the raw API response to GeoJSON
    const yelpGeojson = this.pointsToGeojson(yelpPlaces)
    
    // Convert the GeoJSON to specifically the tile requested
    const mvt = geoJSONToTile(yelpGeojson, z, x, y, { extent: 4096 })
    
    // Convert the tile to a PBF byte array
    return vtpbf.fromGeojsonVt({ [term]: mvt })
  }

@HarelM

HarelM commented Feb 23, 2026

Copy link
Copy Markdown
Collaborator

We want to include:

And then release a version that will be used in maplibre-gl-js to facilitate for geojson updates.
Let me know how urgent this is and we can consider publishing a new version before that.
Meanwhile you can use patch-package to solve this until a new version will be released.
But generally speaking, we are working on it.

@HarelM

HarelM commented Feb 24, 2026

Copy link
Copy Markdown
Collaborator

@jgimbel version 6 should include this, let me know if you have any issues with it.

@jgimbel

jgimbel commented Feb 26, 2026

Copy link
Copy Markdown

Works great for me. I am not in a rush, I was confused because this was merged into main, but not put on npm. It made me wonder if this was forgotten, dead, or on hold for a second. I understand what is happening now.

I can obviously get around this for now by using the old method. It is fast enough for the ~100 geojson I convert.

    const places = await this.placesInTile(z, x, y, term)

    if (!places || places.length === 0) {
      return new Uint8Array()
    }

    const placesGeojson = this.placesToGeojson(places)

    const geojsonVectorStore = global.geojsonvt(yelpGeojson, {
      maxZoom: z,
      indexMaxZoom: z,
      tolerance: 0,
      updateable: false,
    })

    const mvt = geojsonVectorStore.getTile(z, x, y)

    return vtpbf.fromGeojsonVt({ [term]: mvt as unknown as GeoJSONVT })

@HarelM

HarelM commented Feb 26, 2026

Copy link
Copy Markdown
Collaborator

Version 6 was released, you should be able to use it now.

@neodescis

Copy link
Copy Markdown

@lucaswoj It would be interesting to know which cases this is actually more performant. If it applies to many cases, or especially animation cases, then it may be worth adding an option into MapLibre to allow for the use of this method.

Just noticed this change is now available in the MapLibre fork. As such, I will be migrating our project to use it. Our use case for this function is to translate WFS server responses (to requests made at tile boundaries) into vector tiles on the fly. This made adding WFS support to our existing vector tile data pipeline relatively simple.

@lucaswoj

Copy link
Copy Markdown
Contributor Author

@neodescis Great! That is the intended use case of this feature.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants