Загрузка GeoJSON по API

Работа с GeoJSON в MapLibre GL JS строится вокруг концепции источников данных (sources) и слоёв (layers). GeoJSON выступает универсальным форматом, позволяющим описывать геометрические объекты и их свойства, а библиотека обеспечивает их рендеринг на WebGL-карте через декларативные стили.

Ключевой элемент интеграции — источник типа geojson, который может быть как статически заданным объектом, так и динамически загружаемым через API.


Базовая схема подключения GeoJSON через удалённый API

Загрузка данных из внешнего API чаще всего реализуется через комбинацию fetch и метода setData, либо через инициализацию источника с URL.

map.on('load', () => {
  map.addSource('points-source', {
    type: 'geojson',
    data: 'https://example.com/api/points'
  });

  map.addLayer({
    id: 'points-layer',
    type: 'circle',
    source: 'points-source',
    paint: {
      'circle-radius': 6,
      'circle-color': '#2E86AB'
    }
  });
});

В этом варианте MapLibre самостоятельно выполняет HTTP-запрос к указанному URL. Однако такой подход ограничен отсутствием контроля над процессом загрузки и обработки данных.


Управляемая загрузка GeoJSON через fetch

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

map.on('load', async () => {
  const response = await fetch('https://example.com/api/points');
  const geojson = await response.json();

  map.addSource('points-source', {
    type: 'geojson',
    data: geojson
  });

  map.addLayer({
    id: 'points-layer',
    type: 'circle',
    source: 'points-source',
    paint: {
      'circle-radius': 5,
      'circle-color': '#E74C3C'
    }
  });
});

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


Обновление данных без пересоздания источника

Одним из ключевых механизмов работы с динамическими API является метод setData, позволяющий обновлять содержимое источника без удаления слоя.

async function updateData() {
  const response = await fetch('https://example.com/api/points?ts=' + Date.now());
  const geojson = await response.json();

  const source = map.getSource('points-source');
  source.setData(geojson);
}

Этот механизм критически важен для:

  • потоковых данных
  • периодического обновления объектов
  • отображения изменяющихся координат (например, трекинг)

Формирование API-ответа под GeoJSON спецификацию

Корректный серверный ответ должен строго соответствовать спецификации GeoJSON:

{
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "geometry": {
        "type": "Point",
        "coordinates": [73.367, 49.806]
      },
      "properties": {
        "id": 1,
        "name": "Object A"
      }
    }
  ]
}

Нарушение структуры приводит к тихим ошибкам рендеринга без явных сообщений в UI.


Реализация API на стороне сервера

Пример минимального API на Node.js (Express), возвращающего GeoJSON:

import express from 'express';

const app = express();

app.get('/api/points', (req, res) => {
  res.json({
    type: 'FeatureCollection',
    features: [
      {
        type: 'Feature',
        geometry: {
          type: 'Point',
          coordinates: [73.367, 49.806]
        },
        properties: {
          id: 1,
          label: 'Node A'
        }
      }
    ]
  });
});

app.listen(3000);

Важным аспектом является установка корректных заголовков:

res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Content-Type', 'application/json');

Без корректного CORS браузер блокирует загрузку источника.


Динамическая подгрузка данных по границам карты

Типичный сценарий — загрузка объектов только в пределах текущего viewport. Это снижает нагрузку и ускоряет рендеринг.

map.on('moveend', async () => {
  const bounds = map.getBounds();

  const url = new URL('https://example.com/api/points');
  url.searchParams.append('minLng', bounds.getWest());
  url.searchParams.append('minLat', bounds.getSouth());
  url.searchParams.append('maxLng', bounds.getEast());
  url.searchParams.append('maxLat', bounds.getNorth());

  const response = await fetch(url);
  const geojson = await response.json();

  map.getSource('points-source').setData(geojson);
});

Такая модель соответствует подходу “bounding box queries”, часто используемому в геосервисах.


Кэширование GeoJSON и контроль повторных запросов

При частых перемещениях карты необходимо избегать дублирующих запросов. Используются стратегии:

  • in-memory кэш по координатным сеткам
  • debounce событий move
  • ETag / Last-Modified на сервере

Пример debounce:

function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

map.on('move', debounce(updateData, 300));

Обработка ошибок загрузки

API может возвращать некорректные данные или быть временно недоступным. В таких случаях необходимо предотвращать разрушение источника:

async function safeUpdate() {
  try {
    const response = await fetch('https://example.com/api/points');

    if (!response.ok) {
      return;
    }

    const geojson = await response.json();

    const source = map.getSource('points-source');
    if (source) {
      source.setData(geojson);
    }
  } catch (e) {
    console.error('GeoJSON load failed', e);
  }
}

Трансформация данных перед загрузкой

API часто возвращает данные в нестандартной структуре, требующей преобразования:

function normalize(data) {
  return {
    type: 'FeatureCollection',
    features: data.items.map(item => ({
      type: 'Feature',
      geometry: {
        type: 'Point',
        coordinates: [item.lon, item.lat]
      },
      properties: {
        id: item.id,
        title: item.title
      }
    }))
  };
}

Кластеризация при загрузке больших GeoJSON

При работе с тысячами объектов используется встроенная кластеризация MapLibre:

map.addSource('points-source', {
  type: 'geojson',
  data: 'https://example.com/api/points',
  cluster: true,
  clusterMaxZoom: 14,
  clusterRadius: 50
});

Дополнительно создаются слои для кластеров:

map.addLayer({
  id: 'clusters',
  type: 'circle',
  source: 'points-source',
  filter: ['has', 'point_count'],
  paint: {
    'circle-radius': 10,
    'circle-color': '#F39C12'
  }
});

Стратегии оптимизации загрузки GeoJSON

При работе с API-источниками критически важны оптимизации:

  • уменьшение размера GeoJSON (пропуск лишних properties)
  • использование gzip/brotli на сервере
  • генерация тайлов вместо полного GeoJSON
  • ограничение точности координат
  • сегментация данных по регионам

Потоковое обновление объектов

Для сценариев трекинга (например, движение транспорта) используется частичное обновление данных:

setInterval(async () => {
  const response = await fetch('https://example.com/api/live');
  const geojson = await response.json();

  map.getSource('points-source').setData(geojson);
}, 2000);

При высокой частоте обновлений важно избегать полной перерисовки сложных слоёв и использовать минимальный набор объектов.


Интеграция с авторизацией API

Если данные защищены, запросы дополняются токенами:

const response = await fetch('https://example.com/api/points', {
  headers: {
    'Authorization': 'Bearer TOKEN'
  }
});

При этом необходимо учитывать, что MapLibre не управляет авторизацией при использовании URL-источника напрямую, поэтому предпочтителен ручной fetch.


Разделение ответственности между клиентом и API

Эффективная архитектура строится вокруг принципа:

  • API отвечает за выборку и агрегацию данных
  • клиент отвечает за визуализацию и интерактивность

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