Динамическое создание GeoJSON

Работа с MapLibre GL JS практически всегда предполагает взаимодействие с источниками данных формата GeoJSON. При этом статические файлы быстро перестают быть достаточными: координаты обновляются в реальном времени, объекты появляются и исчезают, а атрибуты требуют пересчёта прямо в интерфейсе. В таких сценариях ключевым становится динамическое создание и модификация GeoJSON-структур прямо в JavaScript.

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


Базовая структура GeoJSON и её роль в MapLibre

MapLibre GL JS работает с источниками типа geojson, где данные должны соответствовать стандарту FeatureCollection:

const geojson = {
  type: "FeatureCollection",
  features: [
    {
      type: "Feature",
      geometry: {
        type: "Point",
        coordinates: [66.9237, 48.0196]
      },
      properties: {
        name: "Объект"
      }
    }
  ]
};

Ключевой момент: MapLibre не требует заранее фиксированного файла. Любой объект JavaScript, соответствующий спецификации GeoJSON, может быть передан как источник данных и обновлён в любой момент.


Создание GeoJSON «с нуля» в рантайме

Динамическое формирование чаще всего начинается с пустой коллекции:

const dynamicGeojson = {
  type: "FeatureCollection",
  features: []
};

Далее структура наполняется в зависимости от входных данных.

Генерация точек из массива координат

const points = [
  { lon: 66.9, lat: 48.0, label: "A" },
  { lon: 66.95, lat: 48.02, label: "B" }
];

const geojson = {
  type: "FeatureCollection",
  features: points.map(p => ({
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: [p.lon, p.lat]
    },
    properties: {
      label: p.label
    }
  }))
};

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


Добавление и удаление объектов в реальном времени

MapLibre использует принцип неизменяемости источника: вместо частичного изменения массива требуется обновление всей GeoJSON-структуры через setData.

map.addSource("dynamic-source", {
  type: "geojson",
  data: {
    type: "FeatureCollection",
    features: []
  }
});

Добавление нового объекта

function addPoint(source, lon, lat) {
  const data = source._data; // концептуально; в реальном коде хранят копию отдельно

  data.features.push({
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: [lon, lat]
    },
    properties: {}
  });

  source.setData(data);
}

В корректной архитектуре данные хранятся вне MapLibre:

let state = {
  type: "FeatureCollection",
  features: []
};

function updateSource() {
  map.getSource("dynamic-source").setData(state);
}

Генерация GeoJSON из потоковых данных (WebSocket)

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

const socket = new WebSocket("wss://example.com/stream");

socket.onmess age = (event) => {
  const payload = JSON.parse(event.data);

  state.features.push({
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: [payload.lon, payload.lat]
    },
    properties: {
      timestamp: payload.time
    }
  });

  map.getSource("dynamic-source").setData(state);
};

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


Буферизация обновлений GeoJSON

Частые вызовы setData приводят к перерасчёту слоя и перерисовке карты. Оптимизация заключается в накоплении изменений:

let buffer = [];

function pushData(item) {
  buffer.push(item);
}

setInterval(() => {
  if (buffer.length === 0) return;

  for (const item of buffer) {
    state.features.push(item);
  }

  buffer = [];

  map.getSource("dynamic-source").setData(state);
}, 200);

Такой подход снижает нагрузку и повышает плавность визуализации.


Обновление существующих объектов

GeoJSON не предполагает мутацию объектов «по id» на уровне API, поэтому обновление выполняется через пересборку массива.

function updateFeature(id, newCoords) {
  state.features = state.features.map(f => {
    if (f.properties.id === id) {
      return {
        ...f,
        geometry: {
          type: "Point",
          coordinates: newCoords
        }
      };
    }
    return f;
  });

  map.getSource("dynamic-source").setData(state);
}

Генерация сложных геометрий на основе вычислений

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

Построение окружности вокруг точки

function createCircle(lon, lat, radius, steps = 64) {
  const coords = [];

  for (let i = 0; i <= steps; i++) {
    const angle = (i / steps) * 2 * Math.PI;

    const dx = radius * Math.cos(angle);
    const dy = radius * Math.sin(angle);

    coords.push([lon + dx, lat + dy]);
  }

  return {
    type: "Feature",
    geometry: {
      type: "Polygon",
      coordinates: [coords]
    },
    properties: {}
  };
}

Комбинирование источников данных в единый GeoJSON

Динамическая сборка часто включает агрегацию нескольких источников:

function buildGeojson(apiPoints, userPoints) {
  return {
    type: "FeatureCollection",
    features: [
      ...apiPoints.map(p => ({
        type: "Feature",
        geometry: {
          type: "Point",
          coordinates: [p.lon, p.lat]
        },
        properties: { source: "api" }
      })),
      ...userPoints.map(p => ({
        type: "Feature",
        geometry: {
          type: "Point",
          coordinates: [p.lon, p.lat]
        },
        properties: { source: "user" }
      }))
    ]
  };
}

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


Использование вычисляемых свойств и трансформаций

GeoJSON в MapLibre может содержать свойства, рассчитанные на лету:

function enrichFeature(feature) {
  return {
    ...feature,
    properties: {
      ...feature.properties,
      magnitude: Math.sqrt(
        feature.properties.x ** 2 +
        feature.properties.y ** 2
      )
    }
  };
}

Это позволяет использовать данные для стилизации:

paint: {
  "circle-radius": ["interpolate", ["linear"], ["get", "magnitude"], 0, 5, 10, 20]
}

Минимизация и контроль объёма данных

При динамическом создании GeoJSON критично учитывать размер структуры:

  • удаление устаревших точек
  • ограничение количества features
  • агрегация близких объектов
function trimState(maxItems) {
  if (state.features.length > maxItems) {
    state.features = state.features.slice(-maxItems);
  }
}

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

Перед передачей в MapLibre данные часто проходят несколько этапов:

  1. получение сырых координат
  2. нормализация
  3. фильтрация
  4. преобразование в Feature
  5. объединение в FeatureCollection
function pipeline(raw) {
  return {
    type: "FeatureCollection",
    features: raw
      .filter(r => r.valid)
      .map(r => ({
        type: "Feature",
        geometry: {
          type: "Point",
          coordinates: [r.lon, r.lat]
        },
        properties: {
          speed: r.speed
        }
      }))
  };
}

Взаимодействие с источником MapLibre при частых обновлениях

При интенсивных обновлениях важно учитывать особенности рендеринга:

  • setData вызывает полное обновление источника
  • большие GeoJSON замедляют render loop
  • сложные геометрии увеличивают нагрузку на WebGL

Практика заключается в разделении данных:

  • один слой — динамические точки
  • другой слой — статическая геометрия
  • третий — агрегированные данные

Генерация GeoJSON на основе пользовательских событий

Интерактивные сценарии часто формируют данные через клики:

map.on("click", (e) => {
  state.features.push({
    type: "Feature",
    geometry: {
      type: "Point",
      coordinates: [e.lngLat.lng, e.lngLat.lat]
    },
    properties: {
      created: Date.now()
    }
  });

  map.getSource("dynamic-source").setData(state);
});

Обработка геоданных на стороне клиента

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

  • преобразование координат
  • фильтрация по расстоянию
  • кластеризация
  • генерация тепловых полей
function filterByDistance(center, points, maxDist) {
  return points.filter(p => {
    const dx = p.lon - center[0];
    const dy = p.lat - center[1];
    return Math.sqrt(dx * dx + dy * dy) < maxDist;
  });
}

Итоговая архитектурная модель динамического GeoJSON

В реальных приложениях используется разделение ответственности:

  • state layer — хранит массив features
  • transform layer — преобразует данные
  • map layer — отвечает только за setData
  • stream layer — поставляет обновления

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