fix(gps): слой маркеров Яндекса переведён с Clusterer на ObjectManager — кастомные макеты в Clusterer теряли кликабельность (известный баг), ObjectManager лишён этой болезни
This commit is contained in:
@@ -55,7 +55,7 @@ export function Yandex21Map({
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const mapRef = useRef<ymaps.Map | null>(null);
|
||||
const collectionRef = useRef<ymaps.GeoObjectCollection | null>(null);
|
||||
const clustererRef = useRef<ymaps.Clusterer | null>(null);
|
||||
const objectManagerRef = useRef<any | null>(null);
|
||||
const [mapReady, setMapReady] = useState(false);
|
||||
|
||||
// Колбэки и данные маркеров через ref, чтобы эффекты не перезапускались
|
||||
@@ -112,42 +112,43 @@ export function Yandex21Map({
|
||||
const collection = new ymaps.GeoObjectCollection();
|
||||
map.geoObjects.add(collection);
|
||||
|
||||
// Кластер маркеров объектов (vehicle/person): при отдалении
|
||||
// перекрывающиеся маркеры схлопываются в кружок с числом.
|
||||
// NB: в @types/yandex-maps IClustererOptions неполный — clusterIconLayout,
|
||||
// clusterDisableClickZoom и clusterOpenBalloonOnClick существуют
|
||||
// в рантайме 2.1 (см. документацию), поэтому опции приводим кастом.
|
||||
const clusterer = new ymaps.Clusterer({
|
||||
// Слой кластеризуемых маркеров — ObjectManager (рекомендуемый Яндексом
|
||||
// механизм): в отличие от Clusterer, кастомные iconLayout-маркеры
|
||||
// в нём НЕ теряют кликабельность (известный баг Clusterer).
|
||||
const objectManager = new ymaps.ObjectManager({
|
||||
clusterize: true,
|
||||
gridSize: 64,
|
||||
clusterIconLayout: ymaps.templateLayoutFactory.createClass(
|
||||
gpsClusterHtml('{{ properties.geoObjects.length }}')
|
||||
),
|
||||
// Одиночный клик по кластеру — НЕ зум, а наш balloon со списком
|
||||
// объектов (обработчик ниже); зум — колесом/двойным кликом/контролом
|
||||
// Одиночный клик по кластеру — НЕ зум, а наш список (обработчик ниже)
|
||||
clusterDisableClickZoom: true,
|
||||
clusterOpenBalloonOnClick: false,
|
||||
} as ymaps.IClustererOptions);
|
||||
// В типах map.geoObjects.add не принимает Clusterer (он не IGeoObject
|
||||
// по декларациям, хотя рантайм принимает) — приводим.
|
||||
map.geoObjects.add(clusterer as unknown as ymaps.IGeoObject);
|
||||
} as unknown as ymaps.IObjectManagerOptions);
|
||||
map.geoObjects.add(objectManager as unknown as ymaps.IGeoObject);
|
||||
|
||||
// Клик по кластеру → id маркеров в свой список (balloon карты
|
||||
// в связке с кастомными layout-маркерами ведёт себя ненадёжно)
|
||||
clusterer.events.add('click', (e: ymaps.IEvent) => {
|
||||
const target = e.get('target') as {
|
||||
getGeoObjects?: () => ymaps.IGeoObject[];
|
||||
} | undefined;
|
||||
// Клик по обычному маркеру (не кластеру) — у него нет getGeoObjects
|
||||
if (!target || typeof target.getGeoObjects !== 'function') return;
|
||||
const ids = target
|
||||
.getGeoObjects()
|
||||
.map((g) => (g.properties as unknown as { get(path: string): unknown }).get('gpsMarkerId') as string | number | null)
|
||||
.filter((id): id is string | number => id != null);
|
||||
// Клик по маркеру (в т.ч. вне кластера)
|
||||
objectManager.objects.events.add('click', (e: ymaps.IEvent) => {
|
||||
const id = e.get('objectId');
|
||||
console.log('[GPS-DIAG] om object click', id); // ДИАГНОСТИКА: убрать
|
||||
markersByIdRef.current.get(String(id))?.onClick?.();
|
||||
});
|
||||
// Клик по кластеру → id маркеров в наш вертикальный список
|
||||
objectManager.clusters.events.add('click', (e: ymaps.IEvent) => {
|
||||
const cluster = objectManager.clusters.getById(e.get('objectId')) as
|
||||
| { properties: { geoObjects: Array<{ id?: string | number; properties?: { get(p: string): unknown } }> } }
|
||||
| undefined;
|
||||
const ids = cluster
|
||||
? cluster.properties.geoObjects
|
||||
.map((o) => (o.id !== undefined ? o.id : (o.properties?.get('gpsMarkerId') as string | number | null)))
|
||||
.filter((id): id is string | number => id != null)
|
||||
: [];
|
||||
if (ids.length > 0) onClusterClickRef.current?.(ids);
|
||||
});
|
||||
|
||||
mapRef.current = map;
|
||||
collectionRef.current = collection;
|
||||
clustererRef.current = clusterer;
|
||||
objectManagerRef.current = objectManager;
|
||||
setMapReady(true);
|
||||
onReadyRef.current?.();
|
||||
})
|
||||
@@ -164,7 +165,7 @@ export function Yandex21Map({
|
||||
mapRef.current.destroy();
|
||||
mapRef.current = null;
|
||||
collectionRef.current = null;
|
||||
clustererRef.current = null;
|
||||
objectManagerRef.current = null;
|
||||
}
|
||||
};
|
||||
// Карта создаётся один раз; центр/zoom/bounds обновляет отдельный эффект
|
||||
@@ -223,10 +224,10 @@ export function Yandex21Map({
|
||||
]);
|
||||
useEffect(() => {
|
||||
const collection = collectionRef.current;
|
||||
const clusterer = clustererRef.current;
|
||||
if (!mapReady || !collection || !clusterer) return;
|
||||
const objectManager = objectManagerRef.current;
|
||||
if (!mapReady || !collection || !objectManager) return;
|
||||
collection.removeAll();
|
||||
clusterer.removeAll();
|
||||
objectManager.removeAll();
|
||||
placemarksRef.current.clear();
|
||||
|
||||
for (const p of polygons) {
|
||||
@@ -294,18 +295,41 @@ export function Yandex21Map({
|
||||
return placemark;
|
||||
};
|
||||
|
||||
const omObjects: Array<Record<string, unknown>> = [];
|
||||
for (const m of markers) {
|
||||
// Кластерим только объекты (vehicle/person); dot-маркеры и noCluster —
|
||||
// в обычную коллекцию (точки трека, старт/финиш, вершины геозон)
|
||||
// Кластеризуемые (объекты/задачи) — в ObjectManager; dot-маркеры
|
||||
// (точки трека, старт/финиш, вершины геозон, плейхед) — в коллекцию
|
||||
const clusterable = ((m.kind === 'vehicle' || m.kind === 'person') || m.clusterable === true) && !m.noCluster;
|
||||
const placemark = createPlacemark(m);
|
||||
placemarksRef.current.set(String(m.id), placemark);
|
||||
if (clusterable) {
|
||||
clusterer.add(placemark);
|
||||
const l = gpsMarkerLayout(m);
|
||||
omObjects.push({
|
||||
id: String(m.id),
|
||||
type: 'Point',
|
||||
geometry: [m.lat, m.lng],
|
||||
options: {
|
||||
iconLayout: ymaps.templateLayoutFactory.createClass(gpsMarkerHtml(m)),
|
||||
iconOffset: [-l.anchorX, -l.anchorY],
|
||||
// Активная область от точки привязки (anchor) — без неё hit-test
|
||||
// не находит кастомную метку
|
||||
iconShape: {
|
||||
type: 'Rectangle',
|
||||
coordinates: [
|
||||
[-l.anchorX, -l.anchorY],
|
||||
[l.width - l.anchorX, l.height - l.anchorY],
|
||||
],
|
||||
},
|
||||
...(m.zIndex !== undefined ? { zIndex: m.zIndex } : {}),
|
||||
...(m.popup ? { balloonContent: m.popup } : {}),
|
||||
},
|
||||
properties: { gpsMarkerId: m.id },
|
||||
});
|
||||
} else {
|
||||
const placemark = createPlacemark(m);
|
||||
placemarksRef.current.set(String(m.id), placemark);
|
||||
collection.add(placemark);
|
||||
}
|
||||
}
|
||||
if (omObjects.length > 0) objectManager.add(omObjects);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [dataKey, mapReady]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user