How to set filter only on country boundaries with US worldview? - mapbox

I'm using Mapbox GL to color dynamically countries. Some of the countries are red, some are green.
For this, I'm using the country-boundaries-v1 source, then I use the iso_3166-1 data from country-boundaries to set a filter on the layers that use the country-boundaries source.
This is the code where I add my layers:
addLayers (map, layers) {
layers.forEach((layer) => {
map.addLayer({
id: layer.id,
source: layer.source,
'source-layer': layer.sourceLayer,
layout: {
visibility: 'none'
},
type: 'fill',
paint: {
'fill-opacity': 0.5
}
}, 'country-label')
})
},
And there is where I make visible my layer, add the color and set my filter on a list of iso_3166-1 codes:
map.setLayoutProperty(state.id, 'visibility', 'visible')
map.setPaintProperty(state.id, 'fill-color', this.layerColor(formattedState))
map.setFilter('positive-countries', ['in', 'iso_3166_1', 'FR', 'BE', 'IN', 'CN', 'JP'])
The problem is that when I add the 'fill-color' property on my layers that have reduced opacity, countries with different worldview (like China, Japan, India) therefore have layers that overlap. The result is that some countries, instead of having a layer with an opacity of 0.6, have 3 with an opacity of 0.6, which makes them opaque.
I tried to add this :
map.setFilter('positive-countries', ['match', ['get', 'worldview'], ['all', 'US'], true, false])
Hope Mapbox only uses the US worldview, but it doesn't work.
Here is a screenshot of my map so you can see the problem:
As you can see, countries like China, India and Japan have multiple layers because there's multiple worldview for these countries, so they look different than others.
Does anyone know if what I want to do is possible?
EDIT:
I tried this:
const filters = [
'any',
['in', 'iso_3166_1', 'MA', 'BR'],
['==', 'all', ['get', 'worldview']],
['in', 'US', ['get', 'worldview']]
]
map.setFilter('positive-countries', filters)
But I got these errors:
mapbox-gl.js:31 Error: layers.positive-countries.filter[2][2]: string, number, or boolean expected, array found
mapbox-gl.js:31 Error: layers.positive-countries.filter[3][2]: string, number, or boolean expected, array found
If I remove ['in', 'iso_3166_1', 'MA', 'BR'], there are no more errors but of course I have no applied filter on my map.
So, I tried this:
const filters = [
'any',
['==', 'all', ['get', 'worldview']],
['in', 'US', ['get', 'worldview']]
]
map.setFilter(params['id'], filters)
map.setFilter(params['id'], ['in', 'iso_3166_1', 'MA', 'BR'])
but the result is the same as without filter on the worldview...

If someone else is facing the same problem, this is the solution:
const filters = [
'all',
['match', ['get', 'worldview'], ['all', 'US'], true, false],
['match', ['get', 'iso_3166_1'], ['MA', 'BR'], true, false]
]
this.map.setFilter(layerId, filters)
Only the countries as seen by the USA are then visible

Related

Mapbox - draw lines for each property

I use mapbox-gl to draw a map. In one of the routes (it's a single line) I have 2 attached properties:
{
id: 17,
pcs_mode: 'AtRisk'
},
id: 18,
pcs_mode: 'Excellent'
}
I want to draw 2 lines (each line for a property) and fill using a proper color, depending on pcs_mode.
I've tried to draw these like this:
const layerId = `segment-condition`;
map.current.addLayer({
id: layerId,
type: 'line',
source: SOURCE_NAME,
'source-layer': SOURCE_LAYER_NAME,
layout: {
'line-join': 'round',
'line-cap': 'butt'
},
paint: {
'line-color': [
'case',
['==', ConditionStates.Excellent, ['get', 'pcs_mode']],
CONDITION_COLORS[ConditionStates.Excellent],
['==', ConditionStates.AtRisk, ['get', 'pcs_mode']],
CONDITION_COLORS[ConditionStates.AtRisk],
['==', ConditionStates.Distressed, ['get', 'pcs_mode']],
CONDITION_COLORS[ConditionStates.Distressed],
['==', ConditionStates.NoMsl, ['get', 'pcs_mode']],
CONDITION_COLORS[ConditionStates.NoMsl],
'#000'
],
'line-width': 5,
'line-offset': 5
}
});
But it draws only one line (for the first property I suppose). How to draw lines for each property? Thanks!
In your code you have added only one line layer and the conditions are given for line-color. If you want to visualize two lines , then apply separate line-offset values for each condition state (using expression).

How is this NYT Mapbox map rendered?

The map below is a high resolution county Mapbox map, with dynamically changing data. When trying to render the same high resolution county map with other tools, a lot of processing power is required. Why is this different?
Additionally, I believe they are using a custom tileset to render the county shapes, but then how is the data passed into the visualization? Any insight here would be appreciated. NYT Map Link
Whatever you want to know is in their main.js script
They do not use leaflet but mapbox-gl-js sdkVersion:"2.0.1"
They create a map with their own style
map$1 = new mapboxGl.Map({
container: $wrap,
style: 'mapbox://styles/nytgraphics/cjnxfd2cy57nq2rqj8o3d7sl2',
scrollZoom: false,
boxZoom: false,
maxZoom: 10
});
Then they create their fill layers based on the sources they load
function addSources() {
Object.keys(tilesets).forEach((name) => {
map$1.addSource(name, {
type: 'vector',
url: `mapbox://${tilesets[name]}`,
promoteId: 'geoid'
});
});
map$1.on('sourcedata', () => {
addLayers();
});
}
...
...
map$1.addLayer({
id: 'counties',
type: 'fill',
source: 'counties',
'source-layer': 'USA_Counties_With_RI',
paint: {
'fill-color': [
'case',
['has', ['get', 'geoid'], ['literal', colorLookup]],
['get', ['get', 'geoid'], ['literal', colorLookup]],
'#cccccc'
],
}
});

How to write a Mapbox paint expression that accounts for zoom, feature-state, and data-driven styling?

I have a layer that renders point features in a geojson source as circles. Here's an example of one of the features:
{
type: 'Feature',
properties: {
color: 'red',
red: true,
green: false
},
geometry: {
type: 'Point',
coordinates: [-77.038659, 38.931567]
}
};
I want circle-opacity to be a product of 3 factors (some properties on the features, map zoom, and a boolean in feature-state for whether that feature should be hidden). I can't figure out a way to write an expression that accounts for all three. Restrictions around zoom rules seem to be the issue.
Here's the logic I am trying to write:
if (feature-state.hidden) {
opacity = 0;
} else if (properties.red) {
opacity = 1;
} else if (properties.green and zoom >= 10) {
opacity = 1;
} else if (zoom >= 15) {
opacity = 1;
} else {
opacity = 0;
}
I tried writing an opacity expression like this:
'circle-opacity': [
'case',
['to-boolean', ['feature-state', 'hidden']], 0,
['to-boolean', ['get', 'red']], 1,
['to-boolean', ['get', 'green']], ['case', ['>=', ['zoom'], 10], 1, 0], // I could also write a permutation of this using an ['all'] rule
['>=', ['zoom'], 15], 1,
0,
],
That gets rejected with this message: "Error: layers.places.paint.circle-opacity: "zoom" expression may only be used as input to a top-level "step" or "interpolate" expression."
So then I tried something like this:
'circle-opacity': [
'step',
['zoom'],
0,
[
'case',
['to-boolean', ['get', 'red'], false], 1,
['to-boolean', ['get', 'green'], false], 10,
15
],
1
]
That gets rejected with this message: "Error: layers.places.paint.circle-opacity[3]: Input/output pairs for "step" expressions must be defined using literal numeric values (not computed expressions) for the input values."
As you can see, I hadn't even gotten around to adding the feature-state checks yet.
Here's a JSFiddle to play with: https://jsfiddle.net/rognstad/px4c8tbe/17/
I think I can make it work by breaking each color out into its own layer with a minZoom property and then only using an opacity expression for feature-state, but that feels really clunky and will result in worse performance.
It seems like there has to be a better way. Do you have any better suggestions for how to achieve this? Thanks!
You're really close. You're getting this error:
Error: layers.places.paint.circle-opacity[3]: Input/output pairs for "step" expressions must be defined using literal numeric values (not computed expressions) for the input values.
Because you've got the order of arguments in your ['step'] expression slightly wrong. It needs to go:
['step', ['zoom'], <value at zoom 0>, <X>, <value at zoom X>, <Y>, <value at zoom Y>
So you don't want that first 0 there.
If we refactor your pseudo code a bit you should be able to get there:
step
zoom
// zoom 0 to 10
if (!feature-state.hidden && properties.red) {
1
} else {
0
}
10 // zoom level 10+
if (!feature-state.hidden && (properties.red || properties.green) {
1
} else {
0
}
15 // zoom level 15+
if (!feature-state.hidden) {
1
} else {
0
}
The expression code will look something like this. You'll have to add appropriate type conversions probably. (I haven't tested it).
['step', ['zoom'],
['case', ['all', ['!', ['feature-state', 'hidden']], ['get', 'red'], 1, 0],
10,
['case', ['all', ['!', ['feature-state', 'hidden']], ['any', ['get', 'red'], ['get', 'green']], 1, 0],
15,
['case', [['!', ['feature-state', 'hidden']], 1, 0]],

MapBox GL custom markers with symbol layer

I am trying to cluster custom markers in MapBox GL JS but I cannot figure out how to get a custom marker image from a url into the symbol layer? It either does not work or no markers show up at all. How is it done? I need to know how to use a custom image from a url with the symbol layer. Thank you very much.
map.addSource('parcelpoints', {
type: 'geojson',
data: geojson,
cluster: true,
clusterMaxZoom: 14, // Max zoom to cluster points on
clusterRadius: 50 // Radius of each cluster when clustering points (defaults to 50)
});
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'parcelpoints',
filter: ['has', 'point_count'],
paint: {
// Use step expressions (https://www.mapbox.com/mapbox-gl-js/style-spec/#expressions-step)
// with three steps to implement three types of circles:
// * Blue, 20px circles when point count is less than 100
// * Yellow, 30px circles when point count is between 100 and 750
// * Pink, 40px circles when point count is greater than or equal to 750
'circle-color': [
'step',
['get', 'point_count'],
'#51bbd6',
100,
'#f1f075',
750,
'#f28cb1'
],
'circle-radius': [
'step',
['get', 'point_count'],
20,
100,
30,
750,
40
]
}
});
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'parcelpoints',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': 12
}
});
map.addLayer({
id: 'unclustered-point',
type: 'symbol',
source: 'parcelpoints',
filter: ['!has', 'point_count'],
'layout': {
'visibility': 'visible',
'icon-image': { url: "marker.svg" }
}
});
First you need to:
load the external image with a specific ID, via map.loadImage().
add the image with a specific ID, via map.addImage(https://docs.mapbox.com/mapbox-gl-js/api/map/#map#loadimage).
There is a worked example here: https://www.mapbox.com/mapbox-gl-js/example/add-image/
// If the style's sprite does not already contain an image with ID 'kitty',
// add the image 'cat-icon.png' to the style's sprite with the ID 'kitty'.
map.loadImage('https://upload.wikimedia.org/wikipedia/commons/thumb/6/60/Cat_silhouette.svg/400px-Cat_silhouette.svg.png', (error, image) => {
if (error) throw error;
if (!map.hasImage('kitty')) map.addImage('kitty', image);
});
To include your loaded image in a symbol layer will look something like:
map.addLayer({
'id': 'clusters',
'type': 'circle',
'source': 'parcelpoints',
'filter': ['has', 'point_count'],
'icon-image': 'kitty',
...
There is no support for directly loading symbol images from URLs.

add terrain-rgb layer to MaboxGL.js?

I'd like to use Mapbox's terrain-rgb tiles as a layer in a Mapbox GL map.
The examples I've seen that manipulate terrain-rgb data use L.tileLayer to construct the tiles for the new layer:
https://www.mapbox.com/blog/terrain-rgb/
With MapboxGL.js, I've tried to add the layer this way:
map.on('load', function () {
map.addSource('terrain-rgb', {
type: 'vector',
url: 'https://api.mapbox.com/v4/mapbox.terrain-rgb/{z}/{x}/{y}.pngraw?access_token=pk.eyJ1IjoibWF0dCIsImEiOiJTUHZkajU0In0.oB-OGTMFtpkga8vC48HjIg'
});
map.addLayer({
'id': 'terrain-rgb',
'type': 'fill',
'source': 'terrain-rgb',
'layout': {
'visibility': 'visible'
},
'paint': {
'fill-color': 'rgba(15,148,179,.4)'
},
'source-layer': 'terrain-rgb'
});
});
I tried the fill, line, and circle type, but it's not working.
I get these console errors:
GET https://api.mapbox.com/v4/mapbox.terrain-rgb/%7Bz%7D/%7Bx%7D/%7By%7D.pngraw…ss_token=pk.eyJ1IjoibWF0dCIsImEiOiJTUHZkajU0In0.oB-OGTMFtpkga8vC48HjIg 404 (Not Found) ajax.js:25
Object {type: "error", target: t, isSourceLoaded: false, source: Object, sourceId: "terrain-rgb"} evented.js:104
How do I properly add a the terrain-rgb source to mapboxgl.js?
Ok, after looking through the issues on github, it seems this isn't possible yet:
https://github.com/mapbox/mapbox-gl-js/issues/3730