Редактирование нарисованного

Работа с интерактивной графикой в Google Maps JavaScript API не ограничивается созданием фигур. Существенная часть реальных приложений связана с последующим редактированием уже размещённых объектов: перемещением вершин, изменением формы, корректировкой координат, ограничением допустимых изменений и синхронизацией с внешним хранилищем.

Редактируемыми объектами выступают полигоны, ломаные линии, окружности и прямоугольники. Каждый из них опирается на собственную модель данных, но общие принципы взаимодействия унифицированы через свойства editable, draggable и систему событий изменения геометрии.


Включение режима редактирования объектов

Любой графический объект в карте может быть переведён в режим редактирования через параметр конфигурации:

const polygon = new google.maps.Polygon({
  paths: coordinates,
  editable: true,
  draggable: true,
  map: map
});

Ключевые свойства:

  • editable — разрешает изменение вершин (перетаскивание точек контура)
  • draggable — разрешает перемещение всего объекта целиком

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

  • Полигон: редактируются вершины и внутренние сегменты
  • Polyline: редактируются узлы линии
  • Circle: изменяются центр и радиус через специальные хэндлы
  • Rectangle: изменяются углы прямоугольника

Структура изменяемых данных

Полигон и Polyline

Основой является MVCArray, представляющий динамический массив координат:

const path = polygon.getPath();

MVCArray поддерживает реактивные изменения, и любые правки сразу отражаются на карте.

Основные методы:

  • getAt(index) — получение точки
  • setAt(index, latLng) — замена точки
  • insertAt(index, latLng) — вставка
  • removeAt(index) — удаление

События изменения геометрии

Редактирование сопровождается событиями, позволяющими отслеживать каждое изменение структуры объекта.

Основные события для полигонов и линий

  • set_at — изменение существующей вершины
  • insert_at — добавление новой вершины
  • remove_at — удаление вершины
const path = polygon.getPath();

path.addListener("set_at", () => {
  console.log("вершина изменена");
});

path.addListener("insert_at", () => {
  console.log("добавлена вершина");
});

path.addListener("remove_at", () => {
  console.log("вершина удалена");
});

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


Перемещение объектов целиком

При включённом draggable объект может перемещаться как единое целое без изменения формы.

polygon.setOptions({
  draggable: true
});

События перемещения:

google.maps.event.addListener(polygon, "dragend", () => {
  console.log("объект перемещён");
});

Внутри процесса перетаскивания также доступны:

  • dragstart
  • drag
  • dragend

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

Окружность

Окружность имеет два редактируемых параметра:

  • центр
  • радиус
const circle = new google.maps.Circle({
  center: { lat: 50, lng: 30 },
  radius: 1000,
  editable: true,
  map: map
});

События:

circle.addListener("center_changed", () => {});
circle.addListener("radius_changed", () => {});

Прямоугольник

Редактирование осуществляется через изменение границ:

const rectangle = new google.maps.Rectangle({
  bounds: {
    north: 50,
    south: 49,
    east: 31,
    west: 29
  },
  editable: true,
  map: map
});

Событие изменения:

rectangle.addListener("bounds_changed", () => {});

Drawing Manager как основа редактируемых объектов

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

const drawingManager = new google.maps.drawing.DrawingManager({
  drawingMode: null,
  drawingControl: true,
  polygonOptions: {
    editable: true,
    draggable: true
  }
});

drawingManager.setMap(map);

После создания объекта его можно перевести в режим редактирования:

drawingManager.setDrawingMode(null);
polygon.setEditable(true);

Обновление геометрии в реальном времени

Редактируемые объекты требуют постоянного отслеживания изменений и обновления внешнего состояния.

Сбор координат полигона

function getPolygonCoordinates(polygon) {
  const path = polygon.getPath();
  const coordinates = [];

  for (let i = 0; i < path.getLength(); i++) {
    const latLng = path.getAt(i);
    coordinates.push({
      lat: latLng.lat(),
      lng: latLng.lng()
    });
  }

  return coordinates;
}

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

function attachAutoSync(polygon) {
  const path = polygon.getPath();

  const sync = () => {
    const data = getPolygonCoordinates(polygon);
    saveToServer(data);
  };

  path.addListener("set_at", sync);
  path.addListener("insert_at", sync);
  path.addListener("remove_at", sync);
}

Ограничение редактирования

Фиксация границ карты

const bounds = new google.maps.LatLngBounds(
  { lat: 49, lng: 28 },
  { lat: 51, lng: 32 }
);

polygon.setOptions({
  editable: true
});

Дополнительно ограничения реализуются вручную через проверку координат при событиях set_at.


Запрет выхода за пределы

path.addListener("set_at", (index) => {
  const point = path.getAt(index);

  if (!bounds.contains(point)) {
    path.setAt(index, lastValidPoint);
  }
});

Реализация undo/redo

Редактирование геометрии требует хранения истории изменений.

Простейшая модель истории

const history = [];
let historyIndex = -1;

function pushState(path) {
  const snapshot = [];

  for (let i = 0; i < path.getLength(); i++) {
    const p = path.getAt(i);
    snapshot.push({ lat: p.lat(), lng: p.lng() });
  }

  history.push(snapshot);
  historyIndex++;
}

Восстановление состояния

function restoreState(polygon, state) {
  const path = polygon.getPath();
  path.clear();

  state.forEach(coord => {
    path.push(new google.maps.LatLng(coord.lat, coord.lng));
  });
}

Работа с GeoJSON

Редактируемые объекты часто синхронизируются через GeoJSON.

Экспорт

function polygonToGeoJSON(polygon) {
  return {
    type: "Feature",
    geometry: {
      type: "Polygon",
      coordinates: [
        polygon.getPath().getArray().map(p => [p.lng(), p.lat()])
      ]
    }
  };
}

Импорт

function geoJSONToPolygon(map, geojson) {
  const coords = geojson.geometry.coordinates[0];

  const path = coords.map(c =>
    new google.maps.LatLng(c[1], c[0])
  );

  return new google.maps.Polygon({
    paths: path,
    editable: true,
    map: map
  });
}

Геометрические вычисления при редактировании

Изменение формы требует постоянного пересчёта метрик.

Площадь полигона

const area = google.maps.geometry.spherical.computeArea(polygon.getPath());

Длина линии

const length = google.maps.geometry.spherical.computeLength(polyline.getPath());

Такие вычисления часто привязываются к событиям изменения, обеспечивая динамическое отображение параметров.


Визуальная обратная связь при редактировании

Редактируемые объекты могут менять стиль в процессе взаимодействия:

polygon.setOptions({
  fillOpacity: 0.4,
  strokeWeight: 3
});

При начале редактирования:

polygon.addListener("dragstart", () => {
  polygon.setOptions({ fillOpacity: 0.2 });
});

При завершении:

polygon.addListener("dragend", () => {
  polygon.setOptions({ fillOpacity: 0.4 });
});

Синхронизация с сервером и конфликт изменений

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

  • последнее изменение побеждает (last write wins)
  • версионирование объектов
  • блокировка редактирования
  • диффы по вершинам

Пример формирования diff:

function diffPaths(oldPath, newPath) {
  return {
    added: newPath.filter(p => !oldPath.includes(p)),
    removed: oldPath.filter(p => !newPath.includes(p))
  };
}

Управление точностью координат

При активном редактировании возникает проблема накопления погрешности.

Типовые меры:

  • округление координат до фиксированного количества знаков
  • привязка к сетке
  • снаппинг к ближайшим точкам
function roundLatLng(latLng, precision = 6) {
  return {
    lat: Number(latLng.lat().toFixed(precision)),
    lng: Number(latLng.lng().toFixed(precision))
  };
}

Контроль целостности формы

Редактируемые полигоны могут становиться самопересекающимися. Для предотвращения используются проверки:

  • проверка ориентации (clockwise / counterclockwise)
  • детект пересечений сегментов
  • минимальное количество точек

Динамическая привязка UI к изменениям

Интерфейс часто связан с событиями карты:

path.addListener("insert_at", updateUI);
path.addListener("set_at", updateUI);
path.addListener("remove_at", updateUI);

Функция updateUI может пересчитывать:

  • площадь
  • периметр
  • координаты вершин
  • состояние формы

Управление режимами редактирования

В сложных приложениях переключение режимов выполняется централизованно:

function setEditMode(polygon, enabled) {
  polygon.setEditable(enabled);
  polygon.setDraggable(enabled);
}

Дополнительно может применяться блокировка:

  • только просмотр
  • редактирование вершин
  • свободное перемещение
  • режим построения

Поведение MVCArray при интенсивных изменениях

MVCArray оптимизирован для реактивных обновлений, но при массовых изменениях предпочтительно:

  • временно отключать обработчики
  • выполнять batch-обновления
  • заменять массив целиком
google.maps.event.clearListeners(path);
path.clear();
// массовое заполнение

Обработка ошибок редактирования

Типичные проблемы:

  • выход за допустимые координаты
  • повреждение структуры массива
  • неконсистентные вершины
  • рассинхронизация клиента и сервера

Стратегии:

  • валидация перед применением изменений
  • откат к последнему валидному состоянию
  • периодическое сохранение снапшотов