Location 26.500.0–26.600.0: офисы и плавные маркеры на карте отметок
Location вышел двумя версиями подряд, 26.500.0 и 26.600.0, и обе меняют только JavaScript карты отметок мобильного Битрикс24, той самой, что в 26.400.0 получила движок MapLibre. В 26.500.0 маркер научился ехать к новой точке, линия маршрута растёт по одной вершине, а карта сообщает приложению, что пользователь сдвинул её рукой. В 26.600.0 на карте появились офисы с плашкой «название и кто приписан». PHP-API и схема БД не менялись, так что разбор нужен тем, кто шлёт карте команды, пишет к ней тесты или собирает свои карты на MapLibre и Leaflet.
Новое
Поставьте на карту офис (26.600.0)
Новый тип маркера MarkerType.OFFICE ('office') рисует белую каплю 40×48 px с иконкой здания, точка привязки внизу капли. Под каплей висит плашка с названием офиса и сотрудниками, которые к нему приписаны. Содержимое задают поля markerIcon.data:
titleвыводит плашку, с пустым заголовком её нет совсем;active: trueобводит офис кольцом выделения;- при
isEmployeeListAvailable: trueв плашке стопка аватаров изemployees[]в формате{id, name, avatarUrl}, до пяти лиц, остальные свёрнуты в «+N», а остаток больше 99 подписан «99+»; - без этого флага плашка показывает текст
assignedLabelили числоassignedCount; officeIdпопадает вdata-testid, без него в консоли будетOffice marker has no officeId, но маркер всё равно построится.
«+N» считается от assignedCount, поэтому хосту хватит прислать первые лица и общее число:
// данные команды ADD_MARKERS ('addMarkers') для одного офиса
const data = {
markers: [{
id: 'office-7',
config: {
coords: [55.7579, 37.6136], // [lat, lng]
markerIcon: {
type: 'office',
data: {
officeId: 7,
title: 'Офис на Тверской',
isEmployeeListAvailable: true,
assignedCount: 12, // два лица и «+10»
employees: [
{ id: 101, name: 'Анна Ветрова', avatarUrl: '/upload/main/a1.jpg' },
{ id: 102, name: 'Илья Гусев', avatarUrl: '' },
],
},
},
},
}],
};
Название, подписи и officeId проходят через escapeHtml, так что < в имени офиса разметку не сломает. В Playwright маркер находится по getByTestId('check-in-marker-office-7'). Конфигурацию иконки отдаёт статический MarkerIconFactory.getOfficeIconConfig(office), а одну разметку без размеров возвращает createOfficeIconHtml(office). Оба метода есть в BX.Location.CheckIn, там же теперь лежат константы OFFICE_PLATE_MAX_FACES (5) и PLATE_MIN_ZOOM (13).
Двигайте маркер вместо пересоздания (26.500.0)
Раньше маркер с тем же id в ADD_MARKERS пересоздавался. С animate: true в конфиге он едет в новые координаты за 600 мс с замедлением в конце, animateDuration задаёт своё время. Экземпляр маркера остаётся прежним, поэтому CSS-анимация иконки, например пульс живого сотрудника, не начинается заново. Работает в обоих движках и только для маркера вне кластера. Перед fitBounds и fitToLayers идущие поездки сразу заканчиваются в конечной точке, а включение кластеризации их отменяет. Узнать, едет ли маркер, и остановить поездку можно публичными методами сервисов карты isMarkerAnimating(id) и cancelMarkerAnimation(id).
Наращивайте линию по точке (26.500.0)
Полилиния в ADD_LAYERS получила два поля. appendPoints дописывает вершины к уже нарисованной линии с тем же id, всю линию пересылать не нужно. Если линии с таким id на карте нет, в консоли появится appendPoints has no layer of that id to grow, и команда для этого слоя ничего не сделает. followMarker с id маркера привязывает последнюю вершину к его текущему положению, включая промежуточные кадры поездки, и хвост маршрута не обгоняет сотрудника.
По комментарию в коде, пока приложение гнало всю линию смены через нативный мост на каждую новую точку, хвост получалось обновлять раз в десять секунд, а к концу смены в линии около 1350 вершин. Перерисовку линии за маркером движок ограничивает сам: 30 мс между кадрами до 300 вершин, дальше интервал растёт до 120 мс.
Новая GPS-точка сотрудника теперь укладывается в две короткие команды:
const fix = [55.7601, 37.6187];
// ADD_MARKERS: аватар едет к точке, пульс не сбрасывается
const markersData = {
markers: [{
id: 'employee-42',
config: {
coords: fix,
animate: true,
animateDuration: 400,
markerIcon: {
type: 'employeeavatar',
data: { id: 42, name: 'Анна Ветрова', avatarUrl: '', pulse: true },
},
},
}],
};
// ADD_LAYERS: линия растёт на одну вершину
// followMarker повторяйте в каждой дозаписи, без него привязка снимается
const layersData = {
layers: [{
id: 'track-42',
config: { type: 'polyline', appendPoints: [fix], followMarker: 'employee-42' },
}],
};
Отдайте камеру пользователю (26.500.0)
В CheckInMapEventType из location.core новое событие MAP_MOVED_BY_USER ('mapMovedByUser') с пустыми данными. Карта шлёт его через BXNativeBridge, когда пальцы сдвинули карту или изменили расстояние между собой больше чем на 10 px, а также на прокрутке колёсиком (не чаще раза в 300 мс за жест). Ещё его вызывают команды ZOOM_IN, ZOOM_OUT и FIT_TO_LAYERS. Эти три команды приходят от кнопок нативной панели, нажатие на которые до WebView не доходит, поэтому карта засчитывает их как жест пользователя. Остальные команды камеры, FIT_BOUNDS и SET_ZOOM, события не вызывают. По этому событию приложение, которое ведёт камеру за живым маркером, может выключить слежение. Карта не различает, кто прислал ZOOM_IN, ZOOM_OUT или FIT_TO_LAYERS, так что хост, который шлёт их из своего кода, тоже получит MAP_MOVED_BY_USER.
Включайте пульс аватара отдельно (26.500.0)
У аватара сотрудника разделили выделение и живую позицию. active теперь рисует статичный ореол, как у остальных маркеров. Бегущее кольцо переехало в класс .ci-avatar--pulse и включается полем pulse: true в данных маркера. MarkerIconFactory.createEmployeeAvatarIconHtml() и getEmployeeAvatarIconConfig() получили шестой необязательный параметр pulse. Если ваш код вызывает фабрику сам или передаёт для живого сотрудника только active: true, вместо пульса будет неподвижный ореол. В приложении выделенный аватар больше не пульсирует, пульс остался только у живой позиции.
Шлите offset и в Leaflet (26.500.0)
fitBounds(bounds, options) в Leaflet-движке принимает offset (пиксельный сдвиг кадра, как в MapLibre) и skipClusterTransition. В MapLibre обе опции были и раньше, так что FIT_BOUNDS с ними теперь можно слать, не проверяя, какой движок включён.
Подпись OSM и крупные стрелки в MapLibre (26.500.0)
На карте MapLibre появилась атрибуция OpenStreetMap в правом нижнем углу, раньше её не было нигде. Подпись развёрнута в строку текста 10px. Компактный режим выключен, потому что на телефоне он сворачивает подпись в значок «i», а тот пропадает после первого сдвига карты. Ссылки на maplibre.org в ней нет, потому что в WebView тап по этой ссылке уводил с карты без возможности вернуться.
Стрелки направления на маршруте выросли вдвое, с 12 до 24 px. Стрелка, которая на текущем зуме длиннее своего отрезка, становится прозрачной и появляется, когда пользователь приблизит карту. Длину отрезка хост передаёт в свойстве точки стрелки edgeMercatorMeters, без него стрелка рисуется всегда.
Мелочи и находки
-
Скрытие плашек офисов на дальнем зуме пока только заготовлено (26.600.0).
PLATE_MIN_ZOOM = 13экспортирована, в CSS есть правило для классаci-zoom-no-plate, но этот класс никто не ставит. Плашки видны на любом зуме, и строить свою логику на этой константе рано. -
Тёмной темы у плашки офиса нет. Переопределений для
#check-in-map.theme-darkв 26.600.0 не появилось, фон плашки остаётся#fff. -
Если двигаете
maplibregl.Markerна своей карте, не вешайте на него CSS-transition поtransform(26.500.0). MapLibre переписываетtransformмаркера на каждом кадре движения камеры, и маркер с transition отстаёт от карты. Location интерполирует координату вrequestAnimationFrame. -
Символы MapLibre, которые не помещаются в свой отрезок, прячутся без слушателя
zoom(26.500.0). Сравнить["zoom"]со свойством фичи напрямую нельзя, он разрешён только на входе верхнеуровневогоstepилиinterpolate. Location строитstepпо целым зумам и в каждой ветке ставитcaseсо своим порогом, а ниже 15-го зума слой стрелок не рисуется вовсе (minzoom: 15). Для своего слоя это выглядит так:const ICON_PX = 24; const METERS_PER_PX_Z0 = 78271.517; // мир MapLibre шириной 512 px, не 256 // segmentMeters в фиче: длина отрезка в метрах проекции Меркатора const visibleAt = (zoom) => [ 'case', ['>=', ['number', ['get', 'segmentMeters'], 1e9], (ICON_PX * METERS_PER_PX_Z0) / 2 ** zoom], 1, 0, ]; // ниже первой точки step работает значение по умолчанию: до 15-го зума стрелок нет const fitOpacity = ['step', ['zoom'], 0]; for (let zoom = 15; zoom <= 18; zoom++) { fitOpacity.push(zoom, visibleAt(zoom)); } map.setPaintProperty('route-arrows', 'icon-opacity', fitOpacity);Если взять константу для тайлов 256 px, каждый порог удвоится и стрелки, которые помещаются целиком, тоже пропадут.