How to get consistent elevation info with MapBox? queryTerrainElevation() returns different values depending on zoom level - mapbox

I'm running into a problem where I'm finding that queryTerrainElevation() provides different elevation values depending on the zoom level. The difference can vary as much as 50%.
I assume this is because the terrain map is slightly different at different zooms? Presumably because it is more "averaged" the farther you zoom out?
Is there anything I can do to avoid this problem so that users are getting consistent elevation data regardless of what zoom level they happen to be at? I assume it isn't possible by using queryTerrainElevation() as it can only query what is rendered in the viewport. Is there perhaps another(better) way to load this info?
Here is a quick example I tossed together to show the problem. Simply change the hardcoded zoom and see how the console logged value changes even though it is querying the exact same coordinates.
mapboxgl.accessToken = 'pk.eyJ1IjoicmNvb3BlcjEwMiIsImEiOiJjbDRpbDllaWkwdDR5M2RvYjZ4eXFoY3RlIn0.UQ7qfSyHKph7Acbuhb1swA';
const map = new mapboxgl.Map({
container: 'map',
zoom: 15,
center: [-117.52109099624408, 51.30205367821137],
style: 'mapbox://styles/mapbox/satellite-streets-v12'
});
map.on('style.load', () => {
map.addSource('mapbox-dem', {
'type': 'raster-dem',
'url': 'mapbox://mapbox.mapbox-terrain-dem-v1',
'tileSize': 512,
'maxzoom': 20
});
map.setTerrain({ 'source': 'mapbox-dem', 'exaggeration': 1 });
});
map.on('idle', (e) => {
const elev = map.queryTerrainElevation({lng: -117.52109099624408, lat: 51.30205367821137, });
console.log(elev);
});
Results
At zoom level 2 is reports an elevation of 1785.941989487461
At zoom level 2 is reports an elevation of 1785.941989487461
At zoom level 6 it reports an elevation of 1817.0050096897223
At zoom level 10 it reports an elevation of 1314.4484152921011
At zoom level 15 it reports an elevation of 1316.3900277280711
This is pretty wild variance. Especially at lower zoom values. According to geological data the elevation of the area around this particular coordinate is between 1300 and 1400 so it seems the accuracy is decent at higher zoom levels but terrible at lower ones.
Any suggestions/ideas would be greatly appreciated as this could potentially be a showstopper for the app we are working on. Is there another way to query elevation information? (any solution would need to be robust enough to query potentially many points. Our use case is our app builds an elevation graph for a route)

Related

How to change map color by current zoom level with mapbox gl js?

My question is about mapbox gl js.
How to change map fill-color by current zoom level?
The fill-color of this map has population growth rates by country, with a gradation from minimum to maximum. However, if the zoomed location has similar data, the fill-color difference becomes difficult to understand. So, I want to refer to the tile information according to the zoom level, get the country on the screen, and redraw it.
I searched a lot, but I couldn't figure out how to get the information on the screen. Please let me know if you have any information.
Zoom Level: 1
Zoom Level: 3
Thank you!
Your question initially asks:
How to change map color by current zoom level with mapbox gl js?
For that, you use an expression like 'fill-color': ['interpolate', ['linear'], ['zoom'], ...]
But what you want is something very different:
However, if the zoomed location has similar data, the fill-color difference becomes difficult to understand. So, I want to refer to the tile information according to the zoom level, get the country on the screen, and redraw it.
It sounds like what you want is context-sensitive color scaling. That is, instead of a fixed scale of colors where dark green always means X and light green means Y, instead, dark is the lowest value in the current viewport and light is the highest value in the viewport.
This does not have anything to do with zoom.
The steps you need are:
Detect when the viewport has changed: map.on("moveend", ...)
Find what values exist within the viewport: map.queryRenderedFeatures(...)
Calculate a new expression based on those values.
Set the new expression: map.setPaintProperty(...)

How to bounds in Leaflet?

So ive got a 2000x2000 image as a CRS map where i need to fit -600000 +600000 coordinates, but the image becomes so zoomed that im obviously not doing it right, i guess the program does not know how to combine and calculate that by itself to fit the units into pixels, or does it have a setting for that?
I guess it still makes each pixel of the image approx 600units big but i guess thats accurate enough for what im trying to do.
Coordinates themselves seem to be working as the center of the image is exactly spot on.
var mapOptions = {
center: [11999, 9199],
minZoom: 0,
maxZoom: 0
}
// Creating a map object
var map = L.map('bigmapleaf', {
crs: L.CRS.Simple
}, mapOptions);
var bounds = [[600000,-600000], [-600000,600000]];
var image = L.imageOverlay('../pictures/map.png', bounds).addTo(map);
// Adding layer to the map
map.fitBounds(bounds);
map.setView( center, 1);
Edit:
Now i have managed to find the math that makes the map the right size without any zooming with good precision, but i need to make the map to respond to those kind of large units which i believe requires another conversion back to the original units which does not feel right. I wish i could work with the large units and let leaflet do all the map view conversions. Maybe i try to make a script that discusses both ways with their own units.
Still waiting if someone knows easy answer for my troubles.
In addition to being easier working with them large units, i could also make the map with absolute accuracy.

Displayed zoom level vs tile zoom level: pixel density?

Tiles come with a zoom level, and depending on the area that is viewed, leaflet fills the display with tiles of a certain zoom level.
Currently, the number of pixels in the display and the number of pixels in a tile, are tightly bound together, if I understand correctly. Or actually, it is probably the html/css pixels, which are no longer device pixels.
I believe that these are actually two fundamentally different zoom parameters, especially when (mobile) devices have varying pixel densities (window.devicePixelRatio).
My question is: is it possible to control which zoom level of the tiles is shown, as a function of the zoom level that is displayed (geospatial distance vs screen distance)?
The reason I ask is that the level of detail is often different for different zoom levels. On some devices displaying tiles of higher detail might actually look good. Some map sources, like topographic maps from http://geoportail.gouv.fr even change the map style drastically between different levels. I want to play with the possibility of showing, say, zoom level 15 over a large physical area on a hdpi display, where leaflet would normally show zoom level 14 or 13.
I found that by modifying the option "tileSize", passed to the TileLayer constructor, choosing a value lower than the default 256, I get almost what I want. However: the positioning is way off. Is there a simple solution for this?
After some digging in the source code, I noticed, as IvanSanchez pointed out, that the functionality is present indeed.
detectRetina applies a zoom of 'one up', that is bumping the zoom by one and dividing the length of the sides of the tiles by two, if the device has a devicePixelRatio >= 2.
I want to apply an arbitrary offset at will. This can be done at once for a layer by initializing with the options
let zoomOffset = 2;
let options = {
"detectRetina" : false,
"zoomOffset" : zoomOffset,
"tileSize" : 256 / Math.pow(2, zoomOffset)
}
However, it's even neater to have the possibility to do this realtime while viewing, so I wrote this L.Control-plugin: Leaflet.Control.DetailLevel
I want to play with the possibility of showing, say, zoom level 15 over a large physical area on a hdpi display, where leaflet would normally show zoom level 14 or 13.
It seems that what you want is already implemented by the detectRetina option of L.TileLayers. Quoting the docs::
If true and user is on a retina display, it will request four tiles of half the specified size and a bigger zoom level in place of one to utilize the high resolution.

Is it possible to set a "max allowed pitch" for a Mapbox GL map?

I am unable to find a method to prevent the user from setting the pitch angle of the map too far. I am working with high resolution weather data, so I want to prevent them from setting the pitch so extreme that they can see far away from the intended area. That puts me in a position to either extend the data (WAY too much bandwidth would be used) or just not display it there, making it ugly. I would not like to eliminate the pitch ability completely, as it helps with visualization.
I have looked as much as I can in the documentation but since I cannot find the information and I do not even have a code snippet I have tried because I have nowhere to start. Is there any way to (for example) let the user pitch up to a certain degree only? I made an example image where the left pitch would be OK but not the right, as in that current zoom level it would let them see too far away. If this is not possible, I am open to alternate methods, if any.
Use the render event to check the value of the angle:
map.on('render', (e) => {
if (e.target.getPitch() > MAX_PITCH) e.target.setPitch(MAX_PITCH)
})
[ https://jsfiddle.net/05o4e7dr/ ]
There's now a maxPitch option that you can pass to the Map construct. And, as of Mapbox GL JS 2.0.0, you can set it to pretty high values, all the way up to 85°:
let MAX_PITCH = 85
new mapboxgl
.Map({
container: 'map',
// ...
pitch: MAX_PITCH,
maxPitch: MAX_PITCH,
});

Mapbox.js - Fallback tile image from lower zoom level, when missing in requested zoom level

I serve map png files from disk and I have tile pngs for whole city in zoom level 15. I have also tiles in zoom levels 16-18 but only for certain areas.
I would like to set up the tile Layer, so that when the user is in zoom level 18 the map will display scaled tiles from level 15 as a fallback.
I tried setting option maxNativeZoom, but didn't work for me.
Here is my code:
offlineLayer = L.mapbox.tileLayer(tileJSON, {
minZoom: 8,
maxZoom: 18,
maxNativeZoom: 15
});
map.addLayer(offlineLayer, 'Offline', 1);
Can I make it work, that way using some options or do I need to hack it some way? Or is there some example code for that?
Just to let people know that I wrote Leaflet.TileLayer.Fallback plugin some time ago to address this exact use case:
Replaces missing Tiles (404 error) by scaled lower zoom Tiles.
Demo page
In OP's situation, you would just specify the maxZoom Tile Layer option as usual, and whenever a tile is found missing at the current zoom level, the plugin will try to replace it by the "parent" tile at the previous zoom level (scaled and clipped appropriately so that it fits the missing tile), and so on until a tile is found or minZoom is reached.
L.tileLayer.fallback(urlTemplate, {
minZoom: 8,
maxZoom: 18
});
Disclaimer: I am the author of that plugin.
When you say that it didn't work for you, I'm guessing that what you're seeing with the code above is level 15 tiles for everywhere even when at zoom levels 16-18 in the areas where you have tiles at those levels? That's the expected behavior of maxNativeZoom. TileLayer creates all of the tiles for a given zoom level and map bounds and sets the "src" to a URL containing the current zoom level. If the current zoom level is greater than "maxNativeZoom" then the zoom level in the URL is set to "maxNativeZoom". For example, src="http://a.tile.openstreetmap.org/15/16368/10896.png" would be used for zoom levels 16-18 if "maxNativeZoom" is set to 15.
There is no logic in the code that checks to see if an image actually exists for that tile at that zoom level.
If your tiles are in a single data set then you could modify the code in TileLayer to check for a HTTP 404 return code for the generated URL and if one is received then create a new URL using "maxNativeZoom". If your tiles are in multiple data sets (i.e. one for zoom level 15 and less, and another for zoom levels 16-18) then I think you'd have to write a TileLayer that supports multiple data sets.