-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
MVT Tile Layer #3935
MVT Tile Layer #3935
Conversation
|
Hello @Pessimistress! Sorry for disturbing, but as you gave us insightful feedback in our previous PR I’d like to ask if you or any other members/contributors within the team could tell us their/your thoughts about our current approach and our MVTTileLayer. Thank you! |
@jesusbotella Thank you for the thoughtful PR. Now that we are done with the 8.0 release, I'm happy to resume this conversation. There are several things I feel worth discussing: Should we add a MVTTileLayer to the layer catalog?When we originally released the With that said, I believe you have good reasons to render Mapbox tiles with deck.gl. I think there are a lot of value in sharing this code so that people who have the same needs don't have to jump through the same hoops over and over again. We just have to make it very clear the limitations of this approach. Coordinate systemThere are roughly 3 coordinate systems we deal with in geospatial: world space (degrees in lng-lat), common space ("pixels" on the Web Mercator plane), screen space. When you load a GeoJSON, Mapbox projects world space to common space on the CPU, then common space to screen space on the GPU. deck.gl does both projections on the GPU. As an optimization, Mapbox tiles are encoded in mercator pixel offsets aka common space. Using DecodingWeb worker is definitely going to help performance. We should look at decoding the coordinates directly into flat binary arrays -- another issue of Merging FeaturesThis would be the most controversial one. I have a strong opinion against this approach, as we have previously discussed. I proposed a different solution in another thread -- it would be cheaper to use GPU-based clipping on each tile to remove the overlapping. If autoHighlighting is needed, the MVTTileLayer can implement some internal hover handling to set the So things we need to make this production ready:
|
@Pessimistress thanks a lot for all the comments. I can't agree more with the performance improvements you're suggesting! Our original idea was to have something faster with our main requirements without going down to the internals of the library, so it's great to get your guidance for the next steps to bring this for the community.
Regarding this point, I'd like to add that MVT layers should be a very useful feature to other users because it solves the issue of rendering big datasets in Deck.gl. For example, if you want to visualize census blocks (22M records ~ 8GB). Another interesting use case is time-series datasets, we've time series datasets of 4TB that we're serving via TileLayers, that would be great if we could visualize it via Deck.gl (and integrate them in Kepler). Absolutely agree with you that competing with a base map provider is not the goal. MVT has become the standard in the industry for vector tiles and there are different solutions (commercial and OpenSource) out there for that: CARTO (commercial and Open Source, Mapbox, Tegola, DIY by Paul Ramsey |
Thank you for your review @Pessimistress, we share your concerns about performance as well, so I think we can discuss it :)
Our point for rendering MVT tiles with Deck.gl is to be able to use layers with elevation features, aggregations and some other unique features that Deck.gl has. We are not especially interested in basemaps, but in our own thematic data which are all kinds of points, lines and polygons of diverse domains. MVT provides us a good mechanism to serve the data in our CARTO Platform, with a better performance over other formats for big datasets, so we would be pleased to achieve that using MVTTileLayer integrated in the core library. We are ready to get involved in those tasks with your collaboration. Which are the limitations that this approach has for you?
Yes, that’s something that we should take a look at. I made a little test to get an object with a flat array filled with positions without unprojecting coordinates and another flat array with hole indices from Mapbox’s vector tile library, but I haven’t tried if it would work with IDENTITY coordinate system (COORDINATE_SYSTEM.CARTESIAN). I guess that Deck.gl’s MapView will know how to work with that kind of coordinates.
We improved our performance a lot by implementing WebWorkers to decode MVT tiles. Using flat binary arrays would be much better because creating GeoJSON spec objects is slow. But I am concerned about two topics:
About this last specific topic, how do you think we should approach that properties store? Having a store in JS for properties and then matching properties with features by a certain property?
We agree with you that merging features is costly, so it would be faster to implement GPU clipping to tiles. We left it because that was a low-level feature and we decided to go for merging to see how it performs, but it would be great to see a better performance. It is very relevant for us to get the feature properties, for interaction operations with event handlers (like click, hover, etc.) but also for any other kind of advanced styling and calculations that could be derived from those data, so we saw that the merging option was easier to address than the other one upfront, building on the top of an already working layer, the GeoJSON one. About the clipping, I saw that you added a v8.1 between parenthesis, is it something that you have in your roadmap? Thank you very much! |
@alasarr @jesusbotella Thank you for the additional context. I think this layer will be a nice addition to the deck.gl layer catalog. There are several technical details that need work on, either in this PR or later as a follow up: DecodingUnfortunately we cannot add a webpack-specific dependency (
I think it's worth a try. I suspect it will be visibly faster. You won't be able to use the GeoJsonLayer though - flat coordinates will cause an error in that layer. The MVTLayer will have to sort its features and render
You could put the properties back into a protobuf message. Only the coordinates require heavy processing. My understanding is that re-decoding a message on the main thread is very cheap. And you won't be updating the color/radius/etc. attributes constantly. Coordinate systemI ran some experiments and deck does not seem to handle the combination of MergingFeature clipping is on our 8.1 roadmap (@1chandu). Would you be ok with holding off the merging feature till we compare the performance? |
Yes, an It should be a fairly simple exercise, happy to assist e.g. if you want to put up an initial PR. |
For sure, we think that as well. We would be very pleased seeing these MVTTileLayer changes coming forward. Decoding I would be happy to start working on that and open up a pull request when I have something to share. You can review the code once I open the pull request, and we can work together on merging those changes into master source code. What do you think? Styling features via properties So, for example, when using a Polygon Layer, each data item could have this shape: {
geometry: {
positions: TypedArray,
holeIndices: [ ... ]
},
properties: { ... }
} And then, return That way we would have the same versatility as we had with GeoJSON, and we could retrieve properties easily to be used within Does it make sense? Coordinate system Merging |
@jesusbotella Great! If you put up an initial PR I will take over, wire up the worker loader and get it merged. It should be fairly obvious what to do, but here are some bullets to get you started.
Scaffolding all of this should not take you more than 1-2 hours (it may not build/pass tests at that stage but that is OK), then you can hand over to me. |
Hello again! Just an update from our side. Cartesian Coordinates We’ve been making progress with the coordinate system and created a fork in our organization for @mapbox/vector-tile-js repository so that we can make changes and try them with your modifications for cartesian coordinates. At this point, I think we are in the right way but we’ll need some help/guidance from you! I have two major doubts on how to get those coordinates:
For me, a point in cartesian coordinates can be generated with this formula: x = horizontalOffset + XpointWithinMVT Regarding the offset, if we consider pixel offsets as the index of the tile on screen (vertical or horizontal) * tile size in px, I need the index of the tile within the screen which I don’t know if it is something I can have. Reading Mapbox Vector Tiles specification, they say that geometry data in a Vector Tile is defined in a screen coordinate system. So, given that our tile extent is 2048, and our tile size on screen is 256px, should we convert all points so that they fit in our tile size of 256? I think so but I am not sure if I am right or if it’s better to change our tile extent or not. That conversion operation would give decimal numbers in some cases and I think it’s not ideal. I hope the whole story makes sense. About the PR for the repository containing these changes, we’re not sure if it will get traction from the official maintainers given that the repository is not very active nowadays. If it doesn’t, we could for sure maintain a fork with this improvement so that we can use it for our @loaders.gl MVT parser. Clipping I’ve checked the latest pull requests and I haven’t found any open PR or work in progress about that. Given that you have that in your roadmap for 8.1, when do you plan on releasing Deck.gl 8.1 (just an ETA)? |
@jesusbotella I don't have a clear answer since I don't fully understand what you are trying to do, but here are some observations that might be helpful:
Back to the reason you are making changes... When you say "cartesian coordinates", are you just referring to correctly handle the local coordinates in mapbox tiles, or are you talking about e.g. supporting WGS84 cartesian coordinates? As you can infer I am not at all clear what problem you are trying to solve. Also, I remember mapbox had a good blog post a few years ago explaining the 4096x4096 coordinate system they use in their tiles, but I couldn't find it right now. Suspect it would be worth your while digging it up. |
Yes, sorry, I think my previous message lacked some context 😁 I am talking about what @Pessimistress told in a message above about using Mapbox Vector tile raw coordinates offset with Deck.gl's IDENTITY mode, so that we don't convert them into WGS84 and then back to "mercator pixel" space. I used I hope that it makes more sense :) |
We encountered the same issue when working on #3984. A reasonable approach would be:
Here's what the props will look like: import {Matrix4} from 'math.gl';
new GeoJsonLayer({
...
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN,
modelMatrix: getModelMatrix(tile)
});
function getModelMatrix(tile) {
const extent = tile.extent; // 2048? 4096?
const WORLD_SIZE = 512; // deck.gl constant
const worldScale = Math.pow(2, tile.z);
const xScale = WORLD_SIZE / extent / worldScale;
const yScale = -xScale;
const xOffset = WORLD_SIZE * tile.x / worldScale;
const yOffset = WORLD_SIZE * (1 - tile.y / worldScale);
return modelMatrix = new Matrix4()
.translate([xOffset, yOffset, 0])
.scale([xScale, yScale, 1]);
} Edit: It might be easier if |
Thank you @Pessimistress! That was SUPER helpful! I have already implemented the changes and it works good! I have opened a PR for |
return new Matrix4().translate([xOffset, yOffset, 0]).scale([xScale, yScale, 1]); | ||
} | ||
|
||
function getTileURLIndex({x, y}, templatesLength) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a standard schema? If yes, can you add a link to its specs? Otherwise, should we allow users to customize this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's used to bypass Domain Sharding when HTTP 2 is not available. The user can provide N tiles endpoint and this function does the splitting to each one.
The tilejson spec defines a tiles
attributes for that:
// REQUIRED. An array of tile endpoints. {z}, {x} and {y}, if present,
// are replaced with the corresponding integers. If multiple endpoints are specified, clients
// may use any combination of endpoints. All endpoints MUST return the same
// content for the same URL. The array MUST contain at least one endpoint.
"tiles": [
"http://localhost:8888/admin/1.0.0/world-light,broadband/{z}/{x}/{y}.png"
]
this.state = { | ||
...this.state, | ||
urlTemplates, | ||
getTileData: this.getTileData.bind(this) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you do getTileData.bind(this, urlTemplates)
then getTileData
can be a pure function.
|
||
const defaultProps = Object.assign({}, TileLayer.defaultProps, { | ||
renderSubLayers: {type: 'function', value: renderSubLayers, compare: false}, | ||
urlTemplates: [] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Start a documentation page in docs/layers
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point!
@@ -53,7 +53,7 @@ export default class TileLayer extends CompositeLayer { | |||
tileset.finalize(); | |||
} | |||
tileset = new Tileset2D({ | |||
getTileData, | |||
getTileData: this.state.getTileData || getTileData, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively, we can create a getTileData
method that subclasses can override. By default, it just returns this.props.getTileData()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Let me know if it makes sense for you.
I've just merged master with our branch and resolved the conflicts. I think the code is OK (if you think it doesn't need any changes), but we need to coordinate the merging of this PR (visgl/loaders.gl#646). |
I landed your loaders.gl PRs and published loaders.gl v2.1.0-alpha.5 with your changes included. |
I've been following this issue for a while and am excited to check it out. Congrats everyone who was involved! |
Thank you very much for the merge! 🎉 |
I was just reading through this thread again. Originally @Pessimistress mentioned loading data into flat TypedArrays.
It seems like this got passed over. Was there a conscious decision not to implement this? If not, is there still interest towards this? |
Yes decoding features into flat binary arrays is very interesting. I think this is primarily a loaders.gl issue and we should open an issue there that references this issue. visgl/loaders.gl#685. Converting general geojson (with different feature types) to a flat array leads to a rather complex flat array (many different coordinate array structures). Generating a bunch of different flat arrays optimization for the different feature types could also be an option. |
Following #3695 and #3704
Background
Following what we talked about in the previous PR about polygons/lines merging, we thought of implementing that functionality in another layer called MVTTileLayer, so that we can consume MVTs coming from a backend service and represent them within the map.
We followed TileLayer principals to create this layer as there are a lot of similarities between the two of them. Nevertheless, some differences make this layer work with MVTs without implementing a custom
getTileData
method and avoiding border in represented geometries when clipped by tile limits.The first topic is solved by getting and decoding tile data from an MVT backend URL with a worker to avoid blocking the UI thread.
The second topic is related to our previous PR, because we are joining all tiles' data into a single tile, and then merging geometries by a common property that contains a unique id within GeoJSON properties using turf for polygons and other strategies for other kinds of features. We know that might not be the most efficient approach, but our other approach was to create derived tiles merging split features into one of the tiles that contained it and remove that feature from the other ones.
Other improvements that we had in mind were to execute that geometry merge in a WebWorker or caching composite tiles' (that's how we call our new tile object) render, but we wanted to get your feedback before continuing that path that might not fit everyone's needs.
So it would be great if you could take a look at this and give us your thoughts about it.
We need to improve workers' integration because they are not inlined yet and a new bundle is generated within dist files, but that'll come later.
Change List