Skip to content

Создание кластеризатора маркеров

html
<yandex-map
    v-if="visible"
    v-model="map"
    cursor-grab
    :height="height"
    :settings="{
        location: {
            center,
            zoom: 5,
        },
        theme,
    }"
    :width="width"
>
    <yandex-map-default-scheme-layer/>
    <yandex-map-default-features-layer/>
    <yandex-map-controls :settings="{ position: 'top left' }">
        <yandex-map-control>
            <label class="padded">
                Elements count: {{ count }}
                <input
                    v-model="count"
                    max="30000"
                    min="500"
                    step="500"
                    type="range"
                >
            </label>
            <label class="padded">
                Grid Size: {{ 2 ** gridSize }}
                <!-- @vue-ignore -->
                <input
                    max="11"
                    min="6"
                    type="range"
                    :value="gridSize"
                    @input="gridSize = $event.target.value"
                >
            </label>
        </yandex-map-control>
    </yandex-map-controls>
    <yandex-map-controls :settings="{ position: 'bottom left', orientation: 'vertical' }">
        <yandex-map-control-button>
            Zoom: {{ zoom }}
        </yandex-map-control-button>
        <yandex-map-control-button>
            Bounds: <span class="bounds">{{ JSON.stringify(bounds) }}</span>
        </yandex-map-control-button>
        <yandex-map-control-button>
            True bounds: <span class="bounds">{{ JSON.stringify(trueBounds) }}</span>
        </yandex-map-control-button>
    </yandex-map-controls>
    <yandex-map-controls :settings="{ position: 'right' }">
        <yandex-map-zoom-control/>
    </yandex-map-controls>
    <yandex-map-clusterer
        v-model="clusterer"
        :grid-size="2 ** gridSize"
        zoom-on-cluster-click
        @trueBounds="trueBounds = $event"
    >
        <yandex-map-marker
            v-for="(coordinates) in getPointList"
            :key="coordinates.join(',')"
            :settings="{
                coordinates,
                onClick: () => background = background === 'red' ? 'green' : 'red',
            }"
        >
            <div
                class="marker"
                :style="{
                    '--background': background,
                }"
            />
        </yandex-map-marker>
        <template #cluster="{ length }">
            <div
                class="cluster fade-in"
                :style="{
                    'background': background,
                }"
            >
                {{ length }}
            </div>
        </template>
    </yandex-map-clusterer>
</yandex-map>
ts
import {
    YandexMap,
    YandexMapClusterer,
    YandexMapControl,
    YandexMapControlButton,
    YandexMapControls,
    YandexMapDefaultFeaturesLayer,
    YandexMapDefaultSchemeLayer,
    YandexMapMarker,
    YandexMapZoomControl,
} from 'vue-yandex-maps';
import { computed, onMounted, ref, shallowRef, version, watch } from 'vue';
import type { LngLat, LngLatBounds, YMap } from '@yandex/ymaps3-types';
import type { YMapClusterer } from '@yandex/ymaps3-types/packages/clusterer';

const map = shallowRef<YMap | null>(null);
const clusterer = shallowRef<YMapClusterer | null>(null);
const visible = ref(true);
const count = ref(500);
const savedCount = ref(500);
const gridSize = ref(6);
const background = ref('red');
const zoom = ref(0);
const bounds = ref<LngLatBounds>([[0, 0], [0, 0]]);
const trueBounds = ref<LngLatBounds>([[0, 0], [0, 0]]);

onMounted(() => {
    if (version.startsWith('2')) return;
    setInterval(() => {
        if (map.value) {
            zoom.value = map.value.zoom;
            bounds.value = map.value.bounds;
        }
    }, 1000);
});

watch(count, async val => {
    const oldVal = val;
    setTimeout(() => {
        if (oldVal !== count.value) return;
        savedCount.value = val;
    }, 500);
});

watch(map, val => console.log('map', val));
watch(clusterer, val => console.log('cluster', val));

const seed = (s: number) => () => {
    s = Math.sin(s) * 10000;
    return s - Math.floor(s);
};

const rnd = seed(10000);

const DELTA_LENGTH = 5;

function getRandomPoint() {
    const [x, y] = map.value!.center;
    return [
        x + ((rnd() > 0.5 ? -1 : 1) * rnd() * DELTA_LENGTH * 2),
        y + ((rnd() > 0.5 ? -1 : 1) * rnd() * DELTA_LENGTH),
    ];
}

// @ts-expect-error strict type error
const getPointList = computed<LngLat[]>(() => {
    if (!map.value) return [];
    const result = [];

    for (let i = 0; i < savedCount.value; i++) {
        result.push(getRandomPoint());
    }

    return result;
});
css
<style scoped>
.bounds {
  user-select: all;
}

.marker {
  background: var(--background);
  border-radius: 100%;
  width: 20px;
  height: 20px;
}

.cluster {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 50px;
  height: 50px;
  background: green;
  color: #fff;
  border-radius: 100%;
  cursor: pointer;
  border: 2px solid limegreen;
  outline: 2px solid green;
  text-align: center;
}

.padded {
  padding: 5px;
}

.fade-in {
  animation: fadeIn 0.3s;
}

@keyframes fadeIn {
  0% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}
</style>

Сделано с ♥ под лицензией MIT.