-
-
Notifications
You must be signed in to change notification settings - Fork 65
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
Design Proposal: Contour Line Source from Raster DEM Tiles #583
Comments
This API is what I landed on for configuring the maplibre-contour plugin, but there are a few spots I could go either way on:
|
This is a great write up! Thanks! I agree about the units, I think feet and meters are more readable and are the only options available for the scale control. Other than that, I think this should get it as it will give maplibre a competitive advantage over other libraries. I'll bring it up in the next monthly web meeting, feel free to participate. |
Thanks @HarelM!
What are the issues? One of the reasons for moving out of the plugin into maplibre would be to make use of the shared DEM tile cache between sources.
Just to illustrate the difference, my config from the demo would look like this with where the source sets a {
sources: {
contour_feet: {
type: "contour",
source: "dem",
maxzoom: 16,
multiplier: 3.28084,
thresholds: {
11: [200, 1000],
12: [100, 500],
13: [100, 500],
14: [50, 200],
15: [20, 100],
}
}
},
// ...
layers: [
{
id: "contours",
type: "line",
source: "contour_feet",
"source-layer": "contours",
paint: {
"line-color": "rgba(0,0,0, 50%)",
"line-width": ["match", ["get", "level"], 1, 1, 0.5], // make major contours bolder
},
layout: {
"line-join": "round",
},
},
{
id: "contour-text",
type: "symbol",
source: "contour_feet",
"source-layer": "contours",
filter: [">", ["get", "level"], 0], // only put labels on major contours
layout: {
"symbol-placement": "line",
"text-size": 10,
"text-field": ["number-format", ["get", "ele"], {}],
"text-font": ["Noto Sans Bold"],
},
},
]
} but it would look like this if the major/minor determination is entirely within the layer definition (derived from elevation): {
sources: {
contour_feet: {
type: "contour",
source: "dem",
maxzoom: 16,
multiplier: 3.28084,
thresholds: {
11: 200,
12: 100,
13: 100,
14: 50,
15: 20
}
}
},
// ...
layers: [
{
id: "contours",
type: "line",
source: "contour_feet",
"source-layer": "contours",
paint: {
"line-color": "rgba(0,0,0, 50%)",
// thicker major lines
"line-width": [
"step",
["zoom"],
["match", ["%", ["get", "ele"], 1000], 0, 1, 0.5],
12,
["match", ["%", ["get", "ele"], 500], 0, 1, 0.5],
14,
["match", ["%", ["get", "ele"], 200], 0, 1, 0.5],
15,
["match", ["%", ["get", "ele"], 100], 0, 1, 0.5],
]
}
},
{
id: "contour-text",
type: "symbol",
source: "contour_feet",
"source-layer": "contours",
// only put labels on major lines
filter: [
"step",
["zoom"],
["==", ["%", ["get", "ele"], 1000], 0],
12,
["==", ["%", ["get", "ele"], 500], 0],
14,
["==", ["%", ["get", "ele"], 200], 0],
15,
["==", ["%", ["get", "ele"], 100], 0],
],
layout: {
"symbol-placement": "line",
"text-size": 10,
"text-field": ["number-format", ["get", "ele"], {}],
"text-font": ["Noto Sans Bold"],
},
},
]
} The second one makes layer definitions repeat a lot, and also requires keeping the major/minor level logic in sync with the level thresholds by zoom from the source. For example if you have 50m lines with 250m major lines, but you change the interval to 100m then the 250m/750m major lines will never show up. |
In terrain it is advised to use a different source instead of using the one for hillshade as the logic of which tile to fetch is a bit different due to how the terrain works. Regarding the source and layer coupling, it is the same for other vector sources as well, if you change the definition of a layer in the source you'll need to adapt the style. I'll post it on slack to hopefully get more feedback. |
Is this because we want to switch what zoom level DEM tile we use based on the current zoom differently with terrain vs. hillshade? It will be slightly different for contour lines too if you set the overzoom parameter, but it seems like they should still all be able to pull from a shared tile cache and share the decoding config? |
I'm not entirely sure about the details, but there is an opposite direction between cache and source I think, source cache is managing a source instead of the other way around. |
If you’re considering bathymetry, then fathoms might also be relevant. Apart from that, would you care for a map contoured in smoots? 😎 |
Wow, this looks great to me. Since in most cases we have hillshading /terrain anyways and thus the bytes are already going through the tubes anyways... Ship it. What am I missing? |
OK, this seems like a gap in the current implementation that would be unfortunate to leak into the style spec. It seems like terrain/hillshading/contours reading from the same tiles should be able share a source definition... I'll understand the limitations better when I get into the implementation, but I think it would be good to try to shoot for something consistent with how terrain lets you refer to a raster source by ID, then adjust if that looks like it's causing more problems than it's worth? |
Sure, implementation details of the web library shouldn't affect the spec. |
Another option here would be something like: unit: 'feet' | 'meters' | number So we get the convenience of being able to specify the most common unit by name, but flexibility of being able to use |
What is the performance of downloading 1 PNG for Hillshade and 1 PNG for Countour versus downloading a DEM and the device calculating/drawing? |
The calculations are different, and the browser's cache can help in case it's the same tiles, but this proposal is for native as well so let's try and focus on the style spec changes. |
For my setup currently, it's around:
Although that includes browser cache, and the extra step of encoding the result as vector tile bytes, which won't be necessary any more. 256px tiles should also take 1/4 the time. Vector tile decoding and processing also takes a nontrivial amount of time, often longer than the network request to fetch the tile on web. |
@msbarry I think it is a good idea to add the contour lines plugin's functionality directly to MapLibre GL JS, because
Regarding the implementation in the style spec, I found it sometimes confusing how the major and minor lines were defined in the plugin with the mapping from thresholds to levels. Maybe we could rename |
We discussed at the monthly steering committee meeting, it sounds like there's general consensus to move forward on this. I'll try to simplify the source definition a bit and see what other similar tools do to specify contour levels and major/minor ticks for comparison. My open questions:
|
I think a user using this source should be able to tweak these parameters in theory, having said that, I think we can start off by using some hardcoded "magic" strings in the original definition to avoid the extra complexity, if there's a need to tweak them we can add support for it later on in the next versions. Another input from the meeting yesterday was the zoom definitions, mush like the above, I think we can start of by allowing the logic in the layers and then adding extra complexity if needed in the future. |
Excited to see this coming to MapLibre!
It may be advisable (on the implementation side, so this is a note for future reference) to include a few common constants we document for people, so we don't have to look up meters to feet indefinitely in the future. 😄 |
About the multiplier, see @msbarry 's comment which I think is a good solution for this: |
Oh! I missed that. Yes—that's even better. |
OK great, I updated the definition to include
@lseelenbinder are there any other unit constants you think we should include besides feet and meters to start? |
I think we can omit the major/minor designation for now, but we do need the source to specify at what interval contour lines should be generated. The contour generation performance is proportional to the number of lines that end up on the tile, so if we pick the lowest common denominator then low zoom tiles in hilly areas will slow to a crawl. For comparison, gdal-contour lets you specify:
It also lets you generate polygons (isobands instead of isolines) which I'd probably try to avoid in maplibre unless there's a strong use-case? d3-contour requires you specify a list of "thresholds" (where my original terminology came from). The most flexible version I could see ending up with might look like: levels: {
[zoom: number]: interval | [list, of, thresholds] | { min, max, offset, exponent, ...}
} but we could start by just supporting a single number then add more complex variations based on feedback? Another option instead of a map from zoom level to interval would be to allow a zoom-based expression, like: levels: [
"step", ["zoom"],
200,
11, 100,
14, 50,
15, 20
] WDYT @HarelM ? I'm not sure if it will cause trouble running a maplibre expression outside the context of an individual feature? |
Yea, I wouldn't want to try and use the expression engine in a source property, sounds like asking for trouble. |
These can create really cool effects (e.g., bathymetry layering), but I don't think it's a problem to leave it off the spec because if we actually have a strong use-case, it's easy enough to add later. I also prefer
is actually something different than intervals, IIUC. It's more about specific set of levels (e.g., only |
Ah sorry to clarify I avoided the isobands because I'm not sure if there's a way to do it efficiently enough in the client yet. D3-contour creates all of the contour polygons then tests which ones contain the others to assign shells and holes, which can be expensive compared to naively creating the lines and not having to worry about closing them. There's probably a way to do it with minimal performance impact, I just haven't fully thought through that yet.
👍 changed the definition to intervals: [
200, // value when < z11
11, 100, // >= z11
14, 50, // >= z14
15, 20 // >= z15
] Then people could shorten to intervals: [200] or intervals: 200 if they want the same contours at every zoom? |
I see what you mean with the fact that one needs to define what happens for every zoom level, and a "dictionary" style syntax would be very verbose if you have 10 zoom levels for example. |
I would prefer a more explicit definition like for example this one:
This has on the other hand the downside that the max zoom has to be specified. EDIT: I might be wrong. The syntax that you suggested @msbarry seems closer to the already existing |
I appreciate how this approach would leave open the door to introducing support for more flexibility in the future without requiring a brand-new syntax or some sort of deprecation dance. There’s already plenty of precedent in the style specification and API for allowing an expression but not any arbitrary expression. On the other hand, if someone sees a part of the style specification that allows a |
@1ec5 I was thinking that it would just include the values from within a |
Here's my understanding of the proposal in a way that we can test and see what can and can't be done: Let's try and continue the conversation using these kinds of examples, I think it will be more productive. |
To conclude the above discussion about major and minor lines, given the assumption that major and minor lines are a very common use case I propose the following optional addition to the spec by adding a multiplier that can be changed per zoom level called
|
I like this approach! It provides a solution for the 80% of simple configurations and a solution for the other 20% of complex needs. I'd like to suggest the |
Instead, I suggest that only
It is customary in OSM and in MVT to omit feature attributes that are unset or have a default value . |
Great!! Don't forget to start a PR to this repo for both updating the v8.json file, the spec docs and the validation and diff methods in this repo. |
Looks good to me in general, thanks for the suggestion @HarelM. The only thing that seems off to me is the name Maybe it should rather be be var isMajor = (lineCount % majorModulo === 0) |
Yeah, I'm not super happy with the name either. |
I've asked Gemini:
It replied:
It looks like the use of the multiplicity term is a viable option. |
What about |
That's exactly why I like the "multiplier" part. |
I like majors. Feel free to go with whatever you think is best @msbarry... |
Naming is hard. So adding more terminology to the docket... I think traditionally the thicker contour lines are called index contours:
Following the example below, one advantage here is both N/multiplier/skip can be computed per zoom level under the covers:
|
If the indexInterval (the word index is far from something intuitive to me BTW, but it's not important for this comment) isn't a full step of the interval things become tricky. |
Great thoughts, I agree, that could make a mess of things where as the multiplier directly in the options wouldn't allow the user to get in a pickle as easy. With that said, the index contour terminology still goes. But either way for me, major/index. I just think traditionally index is what has been used. |
I did find that there is one place where a source definition already uses a style expression for the geojson clusterProperties field: maplibre-style-spec/src/validate/validate_source.ts Lines 64 to 78 in 69300a8
Think we should continue with the abbreviated step-zoom syntax? Or turn intervals and major/index into expressions? We still might run into issues because it's not operating in the context of an individual feature... |
I believe it makes more sense to have an expression there if this is not the first time an expression is used, the documentation will need to provide a good example and describe what properties are available in this context (probably only zoom, if I needed to guess). If we encounter serious issues with the implementation we can reduce the complexity of the spec, but I think it makes sense to try. |
I does seem that the term "Index" is used by English speakers, at least in the US. My efforts to find the UK term were unsuccessful... It uses kebab-case, similar to other keywords in the style spec, such as |
What do people think of omitting
So the overzooming to get from a z12 raster DEM tile to a z16, 17, 18+ vector tile could happen in either step 2 or 4. If you set the source maxzoom parameter correctly, the results will look nearly identical, but if you set them wrong they could look bad. I'd propose that we let maplibre manage how much happens in 2 vs. 4 in order to optimize performance and visual appearnce without a user needing to know how the internals work. Is it weird for a source not to have a maxzoom though? |
And for |
I think it makes sense to relay on the definitions of the referenced source to simplify stuff. This is the first source that references another source, so it's the first of it's kind. Light already uses expression in the position property, so I believe using expression here for the interval would make sense as well. |
Perhaps there is no need for No harm will be done if the style layer has no |
It seems like some peaks loose elevation when smoothing/overzoom is applied: The size and shape of the 7,000' contour line around this peak also changes significantly as smoothing is increased.
A similar issue exists for rivers and potentially other natural features. I mitigated that by increasing If the elevation labels are the reason for implementing smoothing/overzooming, then I suggest addressing the issue in the P.S., I would love to see "smoothing" available for line labels in general. Perhaps this is a topic for a different discussion. |
Some maps, including OpenTopoMap and later also Israel Hiking Map, help the reader to identify the "up" direction by orienting the labels according the direction of the slope: I wonder how this label would behave when If the label keeps this orientation, please reverse the direction of the contour lines. The reversal would only affect label layers that use |
Design Proposal: Contour Line Source from Raster DEM Tiles
Motivation
Give users a built-in way to render contour lines in maplibre from the same DEM tiles that are already used for terrain and hillshading, like this:
Proposed Change
Create a new
contour
source type in maplibre style spec that takes a raster-dem source as input and generates contour isolines as output that can be styled using line layers, for example:The generated isolines will have these attributes:
ele
elevation above sea level in the unit specifiedinterval
the fixed interval between isolines at this zoom level in the unit specifiedmajor
true if this is a major isoline based on majorMultiplier at this zoom level, false otherwiseLayers can refer to the contours with
source: contours
but they can omitsourceLayer
.This offloads details about how to retrieve and parse DEM tiles to the DEM source definition, and gives style layers the flexibility to render any number of visible lines derived from that contour source.
I've already prototyped this in the maplibre-contour plugin which I'm using for contour lines on onthegomap.com. Here are some of the issues I had to work through to get these contours to look nice:
DEM "overzooming" (smoothing)
The contour lines look blocky when you zoom in much further past the maximum zoom for a DEM source, but they can look nice and smooth if you "overzoom" the DEM tiles by applying iterative bilinear interpolation before generating isolines. For example for onthegomap I use 512px z11 tiles, but overzoom the z11 tiles up to z15 so that the contour lines look smooth even at high zooms. This is why the proposal lets you specify a
maxzoom
that is higher than themaxzoom
of the raster-dem source.Also to generate smooth contour lines at the border between tiles, the algorithm needs to look at adjacent tiles. This means you need 9 DEM tiles to render a single contour tile. To mitigate this, the
overzoom
parameter lets you use overzoomed DEM tiles from a lower zoom level to generate contours at the current zoom level, for exampleoverzoom=1
means use the top-left, top-right, botom-left, or bottom-right z10 tile to render a z11 contour line tile. This means you only need 4 DEM tiles to render a single contour tile:Contour levels and units
The user needs to be able to choose what elevations to draw contour lines at, which changes by zoom level (rendering every contour would get too expensive at low zooms in hilly areas). They may also designate "major" and "minor" levels, for example generate thin contour lines every 200m but bold every 1000m. For now we will push this to layers that use the style, but in the future we can either add a major/minor designation to ticks, or pass-through the level and interval so styles can highlight every 5th or 10th line or something.
The
unit
attribute multiplies raw meter values by a certain amount to change the unit, for exampleunit=feet
changes from meters to feet. When you click the distance indicator on onthegomap, it toggles betweenunit=meters
andunit=feet
. You could also set unit to a custom value for less common units likeunit=1.8288
for fathoms.Performance and Bundle Size
I've already implemented the smoothing logic and isoline generation in the maplibre-contour plugin so we would just need to bring that into maplibre-gl-js and port into the native projects. The overall plugin is 33kb (11kb gzipped) but most of that is replicating the web worker communication, cancelable message passing, and vector tile encoding that maplibre-gl-js already has. The actual smoothing+isoline business logic is only 3.7kb (1.6kb gzipped).
The isoline generation algorithm was derived from d3-contour but is much more efficient because it generates isolines in a single pass through the DEM tile instead of using a pass per contour level. For onthegomap users on a range of devices (mostly mobile phones) overzooming a 512px dem tile and generating isolines takes:
API Modifications
This should only change the style spec, but shouldn't require any js or native API changes, unless we wanted to expose the default contour layer, elevation or level key as constants?
Migration Plan and Compatibility
This is new functionality, so no migration is necessary.
Rejected Alternatives
Build a plugin for this
I maintain the maplibre-contour plugin which already lets you do this by using the
addProtocol
integration, but it has a few downsides:Pre-render contour lines
You can render contour line vector tiles ahead of time and serve those for the planet, this will save some browser CPU cycles but rendering them on the fly from DEM tiles has a few advantages:
Implement as a new layer type
We could implement this as a new layer type instead of a source type, but that would tightly couple the display parameters to the logic for how contour lines are generated, and potentially require us to generate the contours in multiple passes over the source DEM data. It seems cleaner to generate contour lines so you can generate as many layers as you want from them afterwards.
Take a DEM tile source URL as input
A contour layer could take as input
tiles: ["server.com/{z}/{x}/{y}.png"],
but there are a lot of different knobs to tune for how these are interpreted, so by depending on a DEM source we re-use the DEM source control all of those parameters.The text was updated successfully, but these errors were encountered: