Разработка интерактивных картографических приложений практически всегда связана с управлением состоянием. Пользователь перемещает карту, изменяет масштаб, выбирает объекты, выполняет поиск адресов, строит маршруты и взаимодействует с различными слоями данных. Каждое подобное действие изменяет текущее состояние приложения и требует обновления пользовательского интерфейса.
В экосистеме HERE Maps API реактивность строится вокруг событий, объектов карты и внешних механизмов управления состоянием. Несмотря на то что сама библиотека не предоставляет встроенную реактивную модель наподобие React, Vue или Svelte, её архитектура позволяет эффективно интегрировать любые современные подходы к управлению данными.
Состояние представляет собой набор данных, описывающих текущее положение приложения.
Типичное состояние карты включает:
Пример структуры состояния:
const state = {
center: {
lat: 50.4501,
lng: 30.5234
},
zoom: 12,
selectedMarkerId: null,
markers: [],
route: null,
trafficEnabled: false
};
Все визуальные элементы карты должны отражать актуальное состояние
объекта state.
В HERE Maps состояние может изменяться из нескольких источников одновременно.
Например:
map.addEventListener('tap', event => {
console.log('Карта нажата');
});
После выполнения запроса могут появиться новые маркеры:
fetch('/api/places')
.then(response => response.json())
.then(places => {
state.markers = places;
});
Например, получение координат транспортного средства каждые несколько секунд:
setInterval(updateVehiclePosition, 5000);
Без реактивности изменение данных не влияет автоматически на отображение.
Пример:
state.zoom = 15;
Карта не изменится сама по себе.
Необходимо явно синхронизировать состояние и представление:
map.setZoom(state.zoom);
Более удобным подходом является централизованное обновление интерфейса через функцию рендеринга.
function render() {
map.setCenter(state.center);
map.setZoom(state.zoom);
}
После изменения состояния:
state.zoom = 15;
render();
Во многих проектах объект карты хранится отдельно от бизнес-данных.
Распространённая структура:
const app = {
map: null,
ui: null,
beh * avior: null
};
Инициализация:
app.map = new H.Map(
container,
defaultLayers.vector.normal.map,
{
zoom: 10
}
);
Такой подход позволяет отделить данные приложения от технических объектов HERE Maps.
Большинство реактивных сценариев строится вокруг событий.
map.addEventListener('mapviewchangeend', () => {
const center = map.getCenter();
state.center = {
lat: center.lat,
lng: center.lng
};
});
Теперь состояние всегда содержит актуальные координаты.
map.addEventListener('mapviewchangeend', () => {
state.zoom = map.getZoom();
});
Получается двусторонняя связь:
Количество маркеров часто зависит от данных приложения.
function renderMarkers() {
map.removeObjects(map.getObjects());
state.markers.forEach(item => {
const marker = new H.map.Marker({
lat: item.lat,
lng: item.lng
});
map.addObject(marker);
});
}
Изменение данных:
state.markers.push({
lat: 50.46,
lng: 30.51
});
renderMarkers();
Полное удаление всех объектов может быть дорогой операцией.
Более эффективный подход — обновлять только изменившиеся элементы.
Создание словаря объектов:
const markerStore = new Map();
Добавление:
function addMarker(place) {
const marker = new H.map.Marker({
lat: place.lat,
lng: place.lng
});
markerStore.set(place.id, marker);
map.addObject(marker);
}
Удаление:
function removeMarker(id) {
const marker = markerStore.get(id);
if (marker) {
map.removeObject(marker);
markerStore.delete(id);
}
}
Подобная архитектура хорошо масштабируется для тысяч объектов.
Состояние может хранить выбранный объект.
state.selectedMarkerId = null;
Обработчик выбора:
marker.addEventListener('tap', () => {
state.selectedMarkerId = place.id;
renderSelection();
});
Обновление интерфейса:
function renderSelection() {
console.log(
'Выбран объект',
state.selectedMarkerId
);
}
События HERE Maps позволяют формировать полноценную реактивную систему.
Например:
marker.addEventListener('tap', event => {
const position = event.target.getGeometry();
state.selectedLocation = position;
});
Теперь любое другое представление может использовать это состояние.
Один из популярных подходов — создание простейшего хранилища.
const listeners = [];
Регистрация:
function subscribe(callback) {
listeners.push(callback);
}
Уведомление:
function notify() {
listeners.forEach(listener => listener(state));
}
Обновление состояния:
function setState(partialState) {
Object.assign(state, partialState);
notify();
}
Использование:
subscribe(currentState => {
console.log(currentState);
});
Изменение:
setState({
zoom: 14
});
Современный JavaScript предоставляет объект Proxy.
Он позволяет отслеживать изменения автоматически.
const state = new Proxy(
{
zoom: 10
},
{
set(target, property, value) {
target[property] = value;
render();
return true;
}
}
);
Изменение:
state.zoom = 15;
Автоматически вызовет:
render();
Необходимо избегать ситуаций, когда карта и состояние начинают расходиться.
Нежелательный вариант:
map.setZoom(14);
Состояние осталось прежним.
Лучше использовать единый источник истины.
state.zoom = 14;
render();
Внутри рендера:
function render() {
map.setZoom(state.zoom);
}
Такое решение облегчает сопровождение больших проектов.
В React состояние обычно хранится через useState.
const [center, setCenter] = useState({
lat: 50.45,
lng: 30.52
});
Событие карты:
map.addEventListener('mapviewchangeend', () => {
const position = map.getCenter();
setCenter({
lat: position.lat,
lng: position.lng
});
});
Реакция на изменение:
useEffect(() => {
map.setCenter(center);
}, [center]);
Получается двусторонняя синхронизация между React и HERE Maps.
Во Vue состояние является реактивным по умолчанию.
const state = reactive({
zoom: 10
});
Наблюдение:
watch(
() => state.zoom,
value => {
map.setZoom(value);
}
);
Изменение масштаба:
state.zoom = 15;
Карта обновится автоматически.
В Svelte часто используются Stores.
import { writable } from 'svelte/store';
export const zoom = writable(10);
Подписка:
zoom.subscribe(value => {
map.setZoom(value);
});
Изменение:
zoom.set(14);
Маршрут также удобно хранить в централизованном состоянии.
state.route = null;
После получения маршрута:
router.calculateRoute(
params,
result => {
state.route = result.routes[0];
renderRoute();
}
);
Отрисовка:
function renderRoute() {
if (!state.route) {
return;
}
const lineString =
H.geo.LineString.fromFlexiblePolyline(
state.route.sections[0].polyline
);
const polyline =
new H.map.Polyline(lineString);
map.addObject(polyline);
}
Не только карта обладает состоянием.
Например:
state.sidebarOpened = true;
state.selectedTab = 'traffic';
state.searchText = '';
Все элементы интерфейса могут синхронизироваться через единое хранилище.
Поиск адресов выполняется асинхронно.
Полезно хранить дополнительные признаки.
state.search = {
loading: false,
results: [],
error: null
};
Начало запроса:
state.search.loading = true;
Успешное завершение:
state.search.results = data.items;
state.search.loading = false;
Ошибка:
state.search.error = error.message;
state.search.loading = false;
Такой подход делает интерфейс предсказуемым.
Пример хранения активных слоев:
state.layers = {
traffic: false,
incidents: false,
satellite: false
};
Переключение:
state.layers.traffic =
!state.layers.traffic;
Реакция:
function updateLayers() {
if (state.layers.traffic) {
map.addLayer(trafficLayer);
}
}
В крупных проектах применяется единый Store.
const store = {
state: {
center: null,
zoom: 10,
markers: [],
route: null
},
subscribers: []
};
Подписка:
store.subscribers.push(render);
Обновление:
function update(data) {
Object.assign(store.state, data);
store.subscribers.forEach(
callback => callback()
);
}
Использование:
update({
zoom: 16
});
Частая проблема реактивных систем — бесконечный цикл обновлений.
Пример ошибки:
map.addEventListener(
'mapviewchangeend',
() => {
state.zoom = map.getZoom();
render();
}
);
Внутри рендера:
function render() {
map.setZoom(state.zoom);
}
Если изменение масштаба снова вызывает событие, возникает цикл.
Обычно применяют проверку изменений.
function render() {
if (map.getZoom() !== state.zoom) {
map.setZoom(state.zoom);
}
}
При работе с большим количеством данных необходимо минимизировать количество операций.
Основные рекомендации:
Пример debounce:
function debounce(callback, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(
() => callback(...args),
delay
);
};
}
Использование:
const search =
debounce(runSearch, 500);
Наиболее устойчивой моделью считается схема:
Событие
↓
Изменение состояния
↓
Рендеринг
↓
Обновление карты
Пример:
marker.addEventListener('tap', () => {
setState({
selectedMarkerId: marker.id
});
});
После изменения:
render();
Во время рендеринга:
updateSelection();
updateSidebar();
updateMap();
Подобная архитектура обеспечивает предсказуемость поведения приложения, упрощает отладку и позволяет масштабировать проекты на HERE Maps API без возникновения сложных зависимостей между компонентами интерфейса и картографическими объектами.