Группировка операций

В процессе разработки картографических приложений значительная часть кода связана с выполнением последовательностей взаимосвязанных действий: добавлением источников данных, созданием слоёв, изменением стилей, обработкой событий и обновлением состояния карты. По мере роста проекта количество подобных операций увеличивается, что приводит к дублированию кода, усложнению сопровождения и снижению читаемости.

Группировка операций представляет собой подход к организации кода, при котором связанные действия объединяются в логические блоки, функции, классы или модули. В контексте Mapbox GL JS такой подход позволяет упростить управление картой и повысить масштабируемость приложения.


Причины использования группировки операций

При работе с картой редко выполняется единственное действие. Обычно одна задача включает несколько последовательных шагов.

Например, отображение набора объектов может требовать:

  1. Создания источника данных.
  2. Добавления одного или нескольких слоёв.
  3. Настройки фильтров.
  4. Установки обработчиков событий.
  5. Настройки всплывающих окон.

Без группировки код может выглядеть следующим образом:

map.addSource('cities', {
    type: 'geojson',
    data: citiesData
});

map.addLayer({
    id: 'cities-circle',
    type: 'circle',
    source: 'cities',
    paint: {
        'circle-radius': 8,
        'circle-color': '#007cbf'
    }
});

map.addLayer({
    id: 'cities-label',
    type: 'symbol',
    source: 'cities',
    layout: {
        'text-field': ['get', 'name']
    }
});

map.on('click', 'cities-circle', handleCityClick);
map.on('mouseenter', 'cities-circle', changeCursor);
map.on('mouseleave', 'cities-circle', resetCursor);

Подобный код работает корректно, однако становится трудно управляемым при увеличении количества объектов.


Группировка через функции

Наиболее распространённый способ организации операций — создание функций.

Объединение создания слоя

function addCitiesLayer(map, data) {
    map.addSource('cities', {
        type: 'geojson',
        data
    });

    map.addLayer({
        id: 'cities-circle',
        type: 'circle',
        source: 'cities'
    });

    map.addLayer({
        id: 'cities-label',
        type: 'symbol',
        source: 'cities'
    });
}

Использование:

addCitiesLayer(map, citiesData);

Преимущества:

  • уменьшение объёма основного кода;
  • повторное использование логики;
  • централизованное изменение настроек.

Группировка через объект конфигурации

При большом количестве параметров удобно использовать объект настроек.

Пример

function addPointLayer(map, config) {
    map.addSource(config.sourceId, {
        type: 'geojson',
        data: config.data
    });

    map.addLayer({
        id: config.layerId,
        type: 'circle',
        source: config.sourceId,
        paint: {
            'circle-radius': config.radius,
            'circle-color': config.color
        }
    });
}

Использование:

addPointLayer(map, {
    sourceId: 'restaurants-source',
    layerId: 'restaurants-layer',
    data: restaurantsData,
    radius: 6,
    color: '#ff0000'
});

Такой подход повышает читаемость и упрощает масштабирование функций.


Группировка операций после загрузки карты

Многие действия должны выполняться только после полной инициализации карты.

Стандартный подход

map.on('load', () => {
    addBaseLayers();
    addUserLayers();
    initializeControls();
    initializeEvents();
});

Здесь событие load становится естественной точкой группировки.

Каждая функция отвечает за отдельную область приложения:

function addBaseLayers() {
    // базовые слои
}

function addUserLayers() {
    // пользовательские слои
}

function initializeControls() {
    // элементы управления
}

function initializeEvents() {
    // обработчики событий
}

Группировка операций над источниками данных

Часто требуется выполнить одинаковые действия для нескольких источников.

Без группировки

map.addSource('roads', {...});
map.addSource('rivers', {...});
map.addSource('buildings', {...});

С использованием массива

const sources = [
    {
        id: 'roads',
        config: roadsSource
    },
    {
        id: 'rivers',
        config: riversSource
    },
    {
        id: 'buildings',
        config: buildingsSource
    }
];

sources.forEach(source => {
    map.addSource(source.id, source.config);
});

Такой подход позволяет централизованно управлять коллекцией источников.


Группировка операций над слоями

Mapbox GL JS поддерживает большое количество слоёв различных типов.

Вместо последовательного вызова addLayer можно использовать массив конфигураций.

const layers = [
    {
        id: 'roads',
        type: 'line',
        source: 'roads'
    },
    {
        id: 'rivers',
        type: 'line',
        source: 'rivers'
    },
    {
        id: 'buildings',
        type: 'fill',
        source: 'buildings'
    }
];

layers.forEach(layer => {
    map.addLayer(layer);
});

Преимущества:

  • единый механизм регистрации;
  • удобная динамическая генерация;
  • упрощённое хранение конфигурации.

Группировка удаления объектов

При переключении режимов карты часто требуется удалить несколько элементов одновременно.

Создание функции очистки

function removeMapObjects(map) {
    const layers = [
        'roads',
        'rivers',
        'buildings'
    ];

    layers.forEach(id => {
        if (map.getLayer(id)) {
            map.removeLayer(id);
        }
    });

    const sources = [
        'roads-source',
        'rivers-source',
        'buildings-source'
    ];

    sources.forEach(id => {
        if (map.getSource(id)) {
            map.removeSource(id);
        }
    });
}

Использование:

removeMapObjects(map);

Группировка операций изменения стилей

Нередко необходимо одновременно изменить параметры нескольких слоёв.

Пример

function setVisibility(map, layerIds, visibility) {
    layerIds.forEach(layerId => {
        map.setLayoutProperty(
            layerId,
            'visibility',
            visibility
        );
    });
}

Использование:

setVisibility(
    map,
    ['roads', 'rivers', 'buildings'],
    'none'
);

Логика изменения видимости оказывается сосредоточенной в одном месте.


Группировка операций обновления данных

Для динамических приложений характерно регулярное обновление источников.

Общая функция обновления

function updateGeoJSON(map, sourceId, data) {
    const source = map.getSource(sourceId);

    if (source) {
        source.setData(data);
    }
}

Использование:

updateGeoJSON(
    map,
    'vehicles',
    updatedVehicles
);

Такой подход исключает повторение одинакового кода.


Группировка обработчиков событий

Количество событий в крупном проекте может исчисляться десятками.

Плохая организация

map.on('click', 'points', handleClick);
map.on('mouseenter', 'points', handleEnter);
map.on('mouseleave', 'points', handleLeave);

map.on('click', 'roads', handleRoadClick);
map.on('mouseenter', 'roads', handleRoadEnter);
map.on('mouseleave', 'roads', handleRoadLeave);

Улучшенный вариант

function registerPointEvents(map) {
    map.on('click', 'points', handleClick);

    map.on('mouseenter', 'points', handleEnter);

    map.on('mouseleave', 'points', handleLeave);
}

function registerRoadEvents(map) {
    map.on('click', 'roads', handleRoadClick);

    map.on('mouseenter', 'roads', handleRoadEnter);

    map.on('mouseleave', 'roads', handleRoadLeave);
}

Регистрация становится более структурированной:

registerPointEvents(map);
registerRoadEvents(map);

Группировка операций через классы

Для крупных приложений предпочтительным становится объектно-ориентированный подход.

Пример класса

class CityLayer {
    constructor(map) {
        this.map = map;
    }

    add(data) {
        this.map.addSource('cities', {
            type: 'geojson',
            data
        });

        this.map.addLayer({
            id: 'cities',
            type: 'circle',
            source: 'cities'
        });
    }

    remove() {
        this.map.removeLayer('cities');
        this.map.removeSource('cities');
    }

    update(data) {
        this.map
            .getSource('cities')
            .setData(data);
    }
}

Использование:

const cityLayer = new CityLayer(map);

cityLayer.add(data);
cityLayer.update(newData);
cityLayer.remove();

Преимущества:

  • инкапсуляция логики;
  • повторное использование;
  • удобство тестирования;
  • упрощение сопровождения.

Группировка операций по модулям

Современные проекты обычно используют ES Modules.

Структура проекта

src/
├── map/
│   ├── sources.js
│   ├── layers.js
│   ├── events.js
│   └── controls.js

Модуль слоёв

export function createRoadLayer(map) {
    map.addLayer({
        id: 'roads',
        type: 'line',
        source: 'roads'
    });
}

Импорт

import { createRoadLayer } from './layers.js';

createRoadLayer(map);

Подобная организация особенно важна для корпоративных проектов и сложных геоинформационных систем.


Пакетное выполнение операций

Иногда требуется выполнить набор действий как единую логическую транзакцию.

Пример

function activateTrafficMode(map) {
    showTrafficLayers(map);
    hideTouristLayers(map);
    updateLegend();
    updateControls();
}

Использование:

activateTrafficMode(map);

С точки зрения бизнес-логики происходит одно действие — активация режима движения, хотя технически выполняется множество операций.


Группировка асинхронных операций

При загрузке удалённых данных полезно объединять асинхронные процессы.

Использование Promise.all

async function loadMapData() {
    const [
        roads,
        rivers,
        cities
    ] = await Promise.all([
        fetch('/roads.geojson').then(r => r.json()),
        fetch('/rivers.geojson').then(r => r.json()),
        fetch('/cities.geojson').then(r => r.json())
    ]);

    return {
        roads,
        rivers,
        cities
    };
}

После получения данных можно выполнить единую процедуру регистрации.

async function initializeMap() {
    const data = await loadMapData();

    createRoadLayer(map, data.roads);
    createRiverLayer(map, data.rivers);
    createCityLayer(map, data.cities);
}

Использование менеджеров групп операций

В крупных приложениях часто создаются специальные объекты-координаторы.

Пример

class LayerManager {
    constructor(map) {
        this.map = map;
    }

    initialize() {
        this.createSources();
        this.createLayers();
        this.registerEvents();
    }

    createSources() {
        // создание источников
    }

    createLayers() {
        // создание слоёв
    }

    registerEvents() {
        // регистрация событий
    }
}

Использование:

const manager = new LayerManager(map);

manager.initialize();

Такой подход позволяет объединять десятки связанных операций в единую точку управления.


Рекомендации по организации групп операций

Группировать по назначению

initializeMap();
initializeControls();
initializeLayers();
initializeEvents();

Разделять создание, обновление и удаление

createLayer();
updateLayer();
removeLayer();

Использовать единые соглашения по именованию

addRoadLayer();
addRiverLayer();
addBuildingLayer();

Избегать чрезмерно больших функций

Плохо:

initializeEverything();

Лучше:

initializeSources();
initializeLayers();
initializeInteractions();
initializeControls();

Инкапсулировать сложную логику

Если набор действий относится к одной сущности, его целесообразно размещать внутри класса или отдельного модуля.

Грамотная группировка операций в Mapbox GL JS позволяет создавать предсказуемую архитектуру приложения, уменьшать связанность компонентов, упрощать повторное использование кода и значительно облегчать сопровождение сложных картографических систем.