Загрузка данных через AJAX

Роль AJAX при работе с картами

Интерактивные веб-карты редко используют только статические данные. Основной объём информации поступает динамически: точки интереса, слои с сервера, результаты фильтрации, обновляемые геоданные.

Асинхронная загрузка (AJAX) обеспечивает подгрузку данных без перезагрузки страницы, позволяя обновлять карту по событиям интерфейса, изменению масштаба или перемещению области просмотра.

Под AJAX в современных приложениях обычно понимается набор технологий:

  • fetch API как основной инструмент HTTP-запросов
  • XMLHttpRequest как устаревающий, но поддерживаемый механизм
  • библиотеки (axios и аналоги)
  • форматы данных: JSON, GeoJSON, XML

В контексте картографических приложений ключевым форматом выступает GeoJSON.


GeoJSON как основа динамических слоёв

GeoJSON представляет географические объекты в JSON-структуре:

  • Point — точечные объекты
  • LineString — линии
  • Polygon — полигоны
  • FeatureCollection — набор объектов

Структура Feature:

{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [76.8865, 43.2389]
  },
  "properties": {
    "name": "Объект",
    "category": "poi"
  }
}

AJAX в связке с GeoJSON позволяет динамически формировать слои без перерисовки всей карты.


Загрузка GeoJSON через fetch

Современный подход к получению данных строится на fetch.

Базовая загрузка слоя

fetch('/api/points')
  .then(response => response.json())
  .then(data => {
    L.geoJSON(data).addTo(map);
  })
  .catch(error => {
    console.error('Ошибка загрузки данных:', error);
  });

Ключевой элемент — L.geoJSON, который автоматически интерпретирует структуру и создаёт соответствующие слои.


Добавление кастомной стилизации объектов

GeoJSON-слой в Leaflet поддерживает функцию стилизации:

fetch('/api/zones')
  .then(r => r.json())
  .then(data => {
    L.geoJSON(data, {
      style: feature => {
        return {
          color: feature.properties.color || '#3388ff',
          weight: 2,
          fillOpacity: 0.4
        };
      }
    }).addTo(map);
  });

Для точечных объектов используется pointToLayer:

L.geoJSON(data, {
  pointToLayer: (feature, latlng) => {
    return L.circleMarker(latlng, {
      radius: 6,
      color: '#ff0000'
    });
  }
});

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

Несмотря на устаревание, XHR встречается в legacy-системах.

const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/points');

xhr.onl oad = function () {
  if (xhr.status === 200) {
    const data = JSON.parse(xhr.responseText);
    L.geoJSON(data).addTo(map);
  }
};

xhr.oner ror = function () {
  console.error('Ошибка запроса');
};

xhr.send();

XHR важен в окружениях без поддержки fetch.


Подгрузка данных по событию карты

Карта часто инициирует загрузку при изменении viewport.

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

  fetch(`/api/points?bbox=${bounds.toBBoxString()}`)
    .then(r => r.json())
    .then(data => {
      L.geoJSON(data).addTo(map);
    });
});

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


Обновление слоя без дублирования

При повторных запросах требуется управление слоями:

let layerGroup = L.layerGroup().addTo(map);

function updateData() {
  fetch('/api/points')
    .then(r => r.json())
    .then(data => {
      layerGroup.clearLayers();
      L.geoJSON(data).addTo(layerGroup);
    });
}

L.layerGroup обеспечивает централизованное управление объектами.


AbortController и отмена запросов

При частых перемещениях карты возникает проблема гонки запросов.

let controller;

function loadData() {
  if (controller) controller.abort();

  controller = new AbortController();

  fetch('/api/points', { signal: controller.signal })
    .then(r => r.json())
    .then(data => {
      L.geoJSON(data).addTo(map);
    })
    .catch(err => {
      if (err.name !== 'AbortError') {
        console.error(err);
      }
    });
}

Механизм предотвращает наложение устаревших ответов.


Кэширование и оптимизация сетевых запросов

При работе с картографическими слоями критично уменьшение нагрузки:

Подходы:

  • кэширование результатов по bbox
  • дебаунс запросов при перемещении карты
  • агрегация данных на сервере
  • использование тайловых сервисов вместо точечных запросов

Пример дебаунса:

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

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

Загрузка данных с фильтрацией

Серверные параметры позволяют уменьшить объём GeoJSON:

function loadFiltered(category) {
  fetch(`/api/points?category=${category}`)
    .then(r => r.json())
    .then(data => {
      L.geoJSON(data).addTo(map);
    });
}

Фильтрация может включать:

  • тип объекта
  • временной диапазон
  • географический регион
  • уровень детализации

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

Сетевые ошибки требуют явной обработки:

fetch('/api/points')
  .then(r => {
    if (!r.ok) throw new Error('HTTP error');
    return r.json();
  })
  .then(data => {
    L.geoJSON(data).addTo(map);
  })
  .catch(err => {
    console.error('Ошибка получения данных:', err);
  });

Интеграция с внешними API

Данные могут поступать из различных источников:

  • REST API
  • WFS сервисы
  • кастомные backend-системы
  • сторонние геосервисы

Пример подключения внешнего API:

fetch('https://example.com/geodata')
  .then(r => r.json())
  .then(data => {
    L.geoJSON(data, {
      onEachFeature: (feature, layer) => {
        layer.bindPopup(feature.properties.name);
      }
    }).addTo(map);
  });

Асинхронная загрузка и производительность карты

При увеличении объёма данных ключевым фактором становится контроль рендеринга:

  • использование кластеризации маркеров (Leaflet.markercluster)
  • разделение данных по уровням масштаба
  • подгрузка только видимой области
  • минимизация количества DOM-элементов

Пример кластеризации:

const markers = L.markerClusterGroup();

fetch('/api/points')
  .then(r => r.json())
  .then(data => {
    L.geoJSON(data, {
      onEachFeature: (feature, layer) => {
        markers.addLayer(layer);
      }
    });

    map.addLayer(markers);
  });

Работа с частичными обновлениями данных

Вместо полной перезагрузки слоя возможна инкрементальная модель:

function addNewPoints() {
  fetch('/api/points?since=timestamp')
    .then(r => r.json())
    .then(data => {
      L.geoJSON(data).addTo(map);
    });
}

Такой подход снижает нагрузку и ускоряет обновление интерфейса.


Параллельная загрузка нескольких слоёв

Несколько источников данных могут загружаться одновременно:

Promise.all([
  fetch('/api/roads').then(r => r.json()),
  fetch('/api/buildings').then(r => r.json())
]).then(([roads, buildings]) => {
  L.geoJSON(roads).addTo(map);
  L.geoJSON(buildings).addTo(map);
});

Параллельная загрузка уменьшает общее время ожидания.


Управление состоянием загруженных данных

При работе с динамическими слоями требуется контроль:

  • текущие активные слои
  • версия данных
  • временные метки обновлений
  • соответствие текущему viewport

Пример хранения состояния:

const state = {
  layers: {},
  lastUpdate: null
};

Итеративное обновление данных по таймеру

Автоматическая синхронизация:

setInterval(() => {
  fetch('/api/updates')
    .then(r => r.json())
    .then(data => {
      L.geoJSON(data).addTo(map);
    });
}, 5000);

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