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