Кастомизация результатов в Mapbox GL JS строится вокруг управления слоями (layers), источниками данных (sources) и выражениями (expressions), которые позволяют динамически изменять визуализацию объектов карты на основе их свойств, состояния карты и пользовательского взаимодействия. Основная идея заключается в том, что «результат» на карте — это не фиксированный набор объектов, а производное от стиля, данных и логики их отображения.
Внутренне Mapbox GL JS опирается на стиль в формате Style Specification, где визуализация разбита на:
Каждый «результат» на карте — это объект, прошедший через фильтры слоя и выражения визуализации.
Источники:
map.addSource('places', {
type: 'geojson',
data: '/data/places.geojson'
});
Слои определяют, как именно будут отображены результаты:
map.addLayer({
id: 'places-layer',
type: 'circle',
source: 'places',
paint: {
'circle-radius': 6,
'circle-color': '#1978c8'
}
});
Фильтрация — базовый механизм кастомизации. Она определяет, какие объекты попадут в итоговую выборку отображения.
Фильтры используют логические выражения:
map.setFilter('places-layer', [
'all',
['==', ['get', 'type'], 'restaurant'],
['>=', ['get', 'rating'], 4]
]);
Фильтрация позволяет:
Фильтры применяются на GPU-уровне, что обеспечивает высокую производительность даже при больших наборах данных.
Mapbox GL JS поддерживает runtime-изменение параметров слоёв:
map.setPaintProperty('places-layer', 'circle-color', '#ff3b30');
map.setPaintProperty('places-layer', 'circle-radius', 10);
Также можно изменять layout-параметры:
map.setLayoutProperty('places-layer', 'visibility', 'none');
Эти методы позволяют менять визуальные «результаты» без пересоздания слоя.
Expressions — ключевой инструмент для создания условной логики отображения.
'circle-radius': [
'interpolate',
['linear'],
['get', 'population'],
1000, 4,
100000, 20
]
Размер объекта становится зависимым от данных.
'circle-color': [
'case',
['==', ['get', 'status'], 'active'], '#2ecc71',
['==', ['get', 'status'], 'inactive'], '#e74c3c',
'#95a5a6'
]
'text-field': [
'concat',
['get', 'name'],
' (',
['get', 'category'],
')'
]
Expressions позволяют строить сложные правила визуализации без дополнительного JavaScript-кода.
При работе с интерактивными сценариями часто требуется кастомизация «результатов» взаимодействия, например объектов под курсором.
map.on('click', 'places-layer', (e) => {
const features = map.queryRenderedFeatures(e.point, {
layers: ['places-layer']
});
console.log(features);
});
Функция queryRenderedFeatures возвращает уже
отфильтрованные и отрисованные результаты, что позволяет строить UI
поверх карты.
Дополнительная кастомизация:
const features = map.querySourceFeatures('places', {
sourceLayer: 'places-layer-source'
});
Разница:
queryRenderedFeatures — учитывает стиль и
видимостьquerySourceFeatures — работает с исходными даннымиДля больших наборов данных применяется кластеризация:
map.addSource('points', {
type: 'geojson',
data: '/data/points.geojson',
cluster: true,
clusterMaxZoom: 14,
clusterRadius: 50
});
Отображение кластеров:
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'points',
filter: ['has', 'point_count'],
paint: {
'circle-color': '#51bbd6',
'circle-radius': [
'step',
['get', 'point_count'],
15,
100,
25,
750,
35
]
}
});
Индивидуальные результаты:
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'points',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#f28cb1',
'circle-radius': 6
}
});
Popup часто используется как финальное представление результата:
map.on('click', 'places-layer', (e) => {
const feature = e.features[0];
new mapboxgl.Popup()
.setLngLat(feature.geometry.coordinates)
.setHTML(`
<div>
<strong>${feature.properties.name}</strong><br/>
${feature.properties.description}
</div>
`)
.addTo(map);
});
Кастомизация включает:
Символьные слои позволяют полностью переопределять внешний вид результатов:
map.loadImage('/icons/restaurant.png', (error, image) => {
map.addImage('restaurant-icon', image);
map.addLayer({
id: 'restaurants',
type: 'symbol',
source: 'places',
layout: {
'icon-image': 'restaurant-icon',
'icon-size': 0.8,
'text-field': ['get', 'name'],
'text-offset': [0, 1.2]
}
});
});
Это позволяет превращать геоданные в семантически насыщенные визуальные элементы.
Кастомизация часто зависит от масштаба карты:
'circle-radius': [
'interpolate',
['exponential', 1.5],
['zoom'],
5, 2,
10, 6,
15, 12
]
Или комбинированные условия:
'circle-opacity': [
'case',
['<', ['zoom'], 8], 0.5,
1
]
Это позволяет формировать разные «результаты» для разных уровней детализации.
Разные типы слоёв создают разные модели результата.
map.addLayer({
id: 'polygons',
type: 'fill',
source: 'regions',
paint: {
'fill-color': '#088',
'fill-opacity': 0.4
}
});
map.addLayer({
id: 'routes',
type: 'line',
source: 'routes',
paint: {
'line-color': '#ffcc00',
'line-width': 3
}
});
map.addLayer({
id: 'heat',
type: 'heatmap',
source: 'points',
paint: {
'heatmap-intensity': 1,
'heatmap-radius': 20,
'heatmap-color': [
'interpolate',
['linear'],
['heatmap-density'],
0, 'blue',
1, 'red'
]
}
});
Heatmap фактически агрегирует результаты в непрерывное поле плотности.
Порядок слоёв влияет на итоговое восприятие данных:
map.moveLayer('places-layer', 'road-label');
Также применяется сортировка символов:
'text-allow-overlap': false,
'symbol-sort-key': ['get', 'priority']
State-driven styling позволяет менять результаты в зависимости от состояния:
map.setFeatureState(
{ source: 'places', id: 123 },
{ selected: true }
);
Использование в стиле:
'circle-color': [
'case',
['boolean', ['feature-state', 'selected'], false],
'#ff0000',
'#0000ff'
]
Хотя Mapbox GL JS предоставляет мощные декларативные инструменты, кастомизация часто дополняется JavaScript-логикой:
map.on('moveend', () => {
const bounds = map.getBounds();
const visible = map.queryRenderedFeatures({
layers: ['places-layer']
});
updateUI(visible);
});
Это позволяет синхронизировать визуальные результаты карты с внешними интерфейсами.
Для полного контроля над результатами используется WebGL-слой:
const customLayer = {
id: 'custom-layer',
type: 'custom',
renderingMode: '3d',
onAdd(map, gl) {
this.map = map;
this.gl = gl;
},
render(gl, matrix) {
// WebGL rendering logic
this.map.triggerRepaint();
}
};
map.addLayer(customLayer);
Это уровень полной переопределяемости отображения.
map.setLayoutProperty('place-label', 'text-field', [
'get',
'name:ru'
]);
Или автоматическая локализация:
map.setLayoutProperty('country-label', 'text-field', [
'coalesce',
['get', 'name_ru'],
['get', 'name_en']
]);
Система приоритетов позволяет управлять перегрузкой карты:
'text-max-angle': 30,
'text-padding': 2,
'icon-allow-overlap': false
Это влияет на то, какие результаты остаются видимыми при плотной визуализации данных.
map.on('mouseenter', 'places-layer', () => {
map.getCanvas().style.cursor = 'pointer';
});
map.on('mouseleave', 'places-layer', () => {
map.getCanvas().style.cursor = '';
});
Такие события формируют интерактивное поведение результата и изменяют его восприятие без изменения данных.