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

Open on CodeSandbox

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
    <script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
    <!-- To make the map appear, you must add your apikey -->
    <script src="https://api-maps.yandex.ru/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>

    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="typescript" type="text/babel">
      import type {LngLat} from '@yandex/ymaps3-types';
      import type {Feature} from '@yandex/ymaps3-clusterer';
      import {BOUNDS, LOCATION, getRandomPoints} from './common';

      window.map = null;

      main();
      async function main() {
          // Waiting for all api elements to be loaded
          await ymaps3.ready;

          // Creating a custom control class for cluster management
          interface ClustererChangeControlProps {
              toggleClusterer: () => void;
              changePointsCount: (count: number) => void;
              updatePoints: () => void;
          }

          class ClustererChangeControl extends ymaps3.YMapComplexEntity<ClustererChangeControlProps> {
              private _element: HTMLDivElement;
              private _detachDom: () => void;

              // Method for create a DOM control element
              _createElement(props: ClustererChangeControlProps) {
                  const {toggleClusterer, changePointsCount, updatePoints} = props;

                  const clustererChange = document.createElement('div');
                  clustererChange.classList.add('clusterer-change');

                  const inputSection = document.createElement('div');
                  inputSection.classList.add('clusterer-change__section');

                  const inputLabel = document.createElement('div');
                  inputLabel.classList.add('clusterer-change__input__label');
                  inputLabel.textContent = 'Point count:';
                  inputSection.appendChild(inputLabel);

                  const inputField = document.createElement('input');
                  inputField.type = 'number';
                  inputField.classList.add('clusterer-change__input');
                  inputField.value = '100';
                  inputField.addEventListener('input', (e: Event) => {
                      const target = e.target as HTMLInputElement;
                      changePointsCount(+target.value);
                  });
                  inputSection.appendChild(inputField);

                  const btnSection = document.createElement('div');
                  btnSection.classList.add('clusterer-change__section');

                  const updatePointsBtn = document.createElement('button');
                  updatePointsBtn.type = 'button';
                  updatePointsBtn.classList.add('clusterer-change__btn');
                  updatePointsBtn.textContent = 'Update points';
                  updatePointsBtn.addEventListener('click', updatePoints);
                  btnSection.appendChild(updatePointsBtn);

                  const toggleClustererBtn = document.createElement('button');
                  toggleClustererBtn.type = 'button';
                  toggleClustererBtn.classList.add('clusterer-change__btn');
                  toggleClustererBtn.textContent = 'Delete/Add Clusterer';
                  toggleClustererBtn.addEventListener('click', toggleClusterer);
                  btnSection.appendChild(toggleClustererBtn);

                  clustererChange.appendChild(inputSection);
                  clustererChange.appendChild(btnSection);

                  return clustererChange;
              }

              // Method for attaching the control to the map
              _onAttach() {
                  this._element = this._createElement(this._props);
                  this._detachDom = ymaps3.useDomContext(this, this._element, this._element);
              }

              // Method for detaching control from the map
              _onDetach() {
                  this._detachDom();
                  this._detachDom = null;
                  this._element = null;
              }
          }

          const {YMap, YMapDefaultSchemeLayer, YMapFeatureDataSource, YMapLayer, YMapMarker, YMapControls, YMapControl} =
              ymaps3;

          // Load the package with the cluster, extract the classes for creating clusterer objects and the clustering method
          const {YMapClusterer, clusterByGrid} = await ymaps3.import('@yandex/ymaps3-clusterer@0.0.1');

          // Declare number of points in the clusterer
          let pointsCount = 100;

          map = new YMap(document.getElementById('app'), {location: LOCATION, showScaleInCopyrights: true});
          // Create and add to the map a layer with a map schema, data sources, a layer of markers
          map.addChild(new YMapDefaultSchemeLayer({}))
              .addChild(new YMapFeatureDataSource({id: 'clusterer-source'}))
              .addChild(new YMapLayer({source: 'clusterer-source', type: 'markers', zIndex: 1800}));

          // You can set any markup for the marker and for the cluster
          const contentPin = document.createElement('div');
          contentPin.innerHTML = '<img src="../pin.svg" class="pin">';

          /* We declare the function for rendering ordinary markers, we will submit it to the clusterer settings.
          Note that the function must return any Entity element. In the example, this is ymaps3.YMapMarker. */
          const marker = (feature: Feature) =>
              new YMapMarker(
                  {
                      coordinates: feature.geometry.coordinates,
                      source: 'clusterer-source'
                  },
                  contentPin.cloneNode(true) as HTMLElement
              );

          // As for ordinary markers, we declare a cluster rendering function that also returns an Entity element.
          const cluster = (coordinates: LngLat, features: Feature[]) =>
              new YMapMarker(
                  {
                      coordinates,
                      source: 'clusterer-source'
                  },
                  circle(features.length).cloneNode(true) as HTMLElement
              );

          function circle(count: number) {
              const circle = document.createElement('div');
              circle.classList.add('circle');
              circle.innerHTML = `
                  <div class="circle-content">
                      <span class="circle-text">${count}</span>
                  </div>
              `;
              return circle;
          }

          /* We create a clusterer object and add it to the map object.
          As parameters, we pass the clustering method, an array of features, the functions for rendering markers and clusters.
          For the clustering method, we will pass the size of the grid division in pixels. */
          const clusterer = new YMapClusterer({
              method: clusterByGrid({gridSize: 64}),
              features: getRandomPoints(pointsCount, BOUNDS),
              marker,
              cluster
          });
          map.addChild(clusterer);

          // Creating handler functions for changing the clusterer. We will use these functions in a custom control
          // THe handler function for changing the number of clusterer points
          function changePointsCount(count: number) {
              pointsCount = count;
          }
          // The handler function for updating coordinates of clusterer points
          function updatePoints() {
              clusterer.update({features: getRandomPoints(pointsCount, map.bounds)});
          }
          // The handler function for attach/detach the clusterer
          function toggleClusterer() {
              if (clusterer.parent) {
                  map.removeChild(clusterer);
              } else {
                  map.addChild(clusterer);
              }
          }

          // Creating and adding a custom clusterer change element to the map
          map.addChild(
              new YMapControls({position: 'bottom'}).addChild(
                  new YMapControl().addChild(new ClustererChangeControl({toggleClusterer, changePointsCount, updatePoints}))
              )
          );
      }
    </script>

    <!-- prettier-ignore -->
    <style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; }  </style>
    <link rel="stylesheet" href="./common.css" />
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
    <script crossorigin src="https://cdn.jsdelivr.net/npm/react@17/umd/react.production.min.js"></script>
    <script crossorigin src="https://cdn.jsdelivr.net/npm/react-dom@17/umd/react-dom.production.min.js"></script>
    <script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
    <!-- To make the map appear, you must add your apikey -->
    <script src="https://api-maps.yandex.ru/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>

    <script
      data-plugins="transform-modules-umd"
      data-presets="react, typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="react, typescript" type="text/babel">
      import type {LngLat} from '@yandex/ymaps3-types';
      import type {Feature} from '@yandex/ymaps3-clusterer';
      import {BOUNDS, LOCATION, getRandomPoints} from './common';

      window.map = null;

      main();
      async function main() {
        // For each object in the JS API, there is a React counterpart
        // To use the React version of the API, include the module @yandex/ymaps3-reactify
        const [ymaps3React] = await Promise.all([ymaps3.import('@yandex/ymaps3-reactify'), ymaps3.ready]);
        const reactify = ymaps3React.reactify.bindTo(React, ReactDOM);
        const {
          YMap,
          YMapDefaultSchemeLayer,
          YMapFeatureDataSource,
          YMapLayer,
          YMapMarker,
          YMapControls,
          YMapControl
        } = reactify.module(ymaps3);

        // Load the package with the cluster, extract the classes for creating clusterer objects and the clustering method
        const {YMapClusterer, clusterByGrid} = reactify.module(await ymaps3.import('@yandex/ymaps3-clusterer@0.0.1'));

        const {useState, useMemo, useCallback} = React;

        function App() {
          // We declare the initial states of the clusterer
          // Clusterer visibility
          const [isClusterer, setIsClusterer] = useState(true);
          // Number of points in the clusterer
          const [pointsCount, setPointsCount] = useState(100);
          // Array with parameters for each clusterer point
          const [points, setPoints] = useState(getRandomPoints(pointsCount, BOUNDS));

          // We declare a render function. For the clustering method, we pass and store the size of one grid division in pixels
          const gridSizedMethod = useMemo(() => clusterByGrid({gridSize: 64}), []);

          // We declare a function for rendering ordinary markers. Note that the function must return any Entity element. In the example, this is ymaps3.YMapMarker
          const marker = useCallback(
            (feature: Feature) => (
              <YMapMarker key={feature.id} coordinates={feature.geometry.coordinates} source="clusterer-source">
                <img src="../pin.svg" className="pin" />
              </YMapMarker>
            ),
            []
          );

          // We declare a cluster rendering function that also returns an Entity element. We will transfer the marker and cluster rendering functions to the clusterer settings
          const cluster = useCallback(
            (coordinates: LngLat, features: Feature[]) => (
              <YMapMarker
                key={`${features[0].id}-${features.length}`}
                coordinates={coordinates}
                source="clusterer-source"
              >
                <div className="circle">
                  <div className="circle-content">
                    <span className="circle-text">{features.length}</span>
                  </div>
                </div>
              </YMapMarker>
            ),
            []
          );

          // Creating handler functions for changing the clusterer. We will use these functions in a custom control
          // THe handler function for changing the number of clusterer points
          const changePointsCount = useCallback(
            (event: React.ChangeEvent<HTMLInputElement>) => setPointsCount(+event.target.value),
            []
          );
          // The handler function for updating coordinates of clusterer points
          const updatePoints = useCallback(() => setPoints(getRandomPoints(pointsCount, map.bounds)), [pointsCount]);
          // The handler function for attach/detach the clusterer
          const toggleClusterer = useCallback(() => setIsClusterer((prevValue) => !prevValue), []);

          return (
            // Initialize the map and pass initialization parameters
            <YMap location={LOCATION} showScaleInCopyrights={true} ref={(x) => (map = x)}>
              {/* Add a map scheme layer */}
              <YMapDefaultSchemeLayer />
              {/* Add clusterer data sources */}
              <YMapFeatureDataSource id="clusterer-source" />
              {/* Add the layer for markers and the clusterer */}
              <YMapLayer source="clusterer-source" type="markers" zIndex={1800} />
              {/* In the clusterer props, we pass the previously declared functions for rendering markers and clusters,
                the clustering method, and an array of features */}
              {isClusterer && (
                <YMapClusterer marker={marker} cluster={cluster} method={gridSizedMethod} features={points} />
              )}

              {/* Add a custom clusterer change element to the map */}
              <YMapControls position="bottom">
                <YMapControl>
                  <div className="clusterer-change">
                    <div className="clusterer-change__section">
                      <div className="clusterer-change__input__label">Point count:</div>
                      <input
                        type="number"
                        className="clusterer-change__input"
                        value={pointsCount}
                        onChange={changePointsCount}
                      ></input>
                    </div>
                    <div className="clusterer-change__section">
                      <button type="button" className="clusterer-change__btn" onClick={updatePoints}>
                        Update points
                      </button>
                      <button type="button" className="clusterer-change__btn" onClick={toggleClusterer}>
                        Delete/Add Clusterer
                      </button>
                    </div>
                  </div>
                </YMapControl>
              </YMapControls>
            </YMap>
          );
        }

        ReactDOM.render(
          <React.StrictMode>
            <App />
          </React.StrictMode>,
          document.getElementById('app')
        );
      }
    </script>

    <!-- prettier-ignore -->
    <style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; }  </style>
    <link rel="stylesheet" href="./common.css" />
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
    <script crossorigin src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
    <script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
    <!-- To make the map appear, you must add your apikey -->
    <script src="https://api-maps.yandex.ru/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>

    <script
      data-plugins="transform-modules-umd"
      data-presets="react, typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="react, typescript" type="text/babel">
      import type {LngLat} from '@yandex/ymaps3-types';
      import type {Feature} from '@yandex/ymaps3-clusterer';
      import {BOUNDS, LOCATION, getRandomPoints} from './common';

      window.map = null;

      async function main() {
        // For each object in the JS API, there is a Vue counterpart
        // To use the Vue version of the API, include the module @yandex/ymaps3-vuefy
        const [ymaps3Vue] = await Promise.all([ymaps3.import('@yandex/ymaps3-vuefy'), ymaps3.ready]);
        const vuefy = ymaps3Vue.vuefy.bindTo(Vue);
        const {
          YMap,
          YMapDefaultSchemeLayer,
          YMapFeatureDataSource,
          YMapLayer,
          YMapMarker,
          YMapControls,
          YMapControl
        } = vuefy.module(ymaps3);
        const {YMapClusterer, clusterByGrid} = vuefy.module(await ymaps3.import('@yandex/ymaps3-clusterer@0.0.1'));

        const Marker = Vue.defineComponent({
          props: ['feature'],
          components: {
            YMapMarker
          },
          template: `
            <YMapMarker :key="feature.id" :coordinates="feature.geometry.coordinates" source="clusterer-source">
                <img src="../pin.svg" class="pin" alt=""/>
            </YMapMarker>
        `
        });

        const Cluster = Vue.defineComponent({
          props: {
            coordinates: Array,
            features: Array
          },
          components: {
            YMapMarker
          },
          template: `
          <YMapMarker
              :key="features[0].id + '-' + features.length"
              :coordinates="coordinates"
              source="clusterer-source"
          >
            <div class="circle">
              <div class="circle-content">
                <span class="circle-text">{{features.length}}</span>
              </div>
            </div>
          </YMapMarker>
        `
        });

        const App = Vue.createApp({
          components: {
            YMap,
            YMapDefaultSchemeLayer,
            YMapFeatureDataSource,
            YMapLayer,
            YMapClusterer,
            YMapControls,
            YMapControl,
            Cluster,
            Marker
          },
          setup() {
            const refMap = (ref) => {
              window.map = ref?.entity;
            };

            const isClusterer = Vue.ref(true);
            const pointsCount = Vue.ref(100);
            const points = Vue.ref(getRandomPoints(pointsCount.value, BOUNDS));

            const changePointsCount = (event) => {
              pointsCount.value = +event.target.value;
            };

            const updatePoints = () => {
              points.value = getRandomPoints(pointsCount.value, map.bounds);
            };

            const toggleClusterer = () => {
              isClusterer.value = !isClusterer.value;
            };
            const gridSizedMethod = clusterByGrid({gridSize: 64});

            Vue.watch(pointsCount, () => {
              updatePoints();
            });

            return {
              refMap,
              LOCATION,
              isClusterer,
              points,
              pointsCount,
              changePointsCount,
              updatePoints,
              toggleClusterer,
              Marker,
              Cluster,
              gridSizedMethod
            };
          },
          template: `
          <YMap :location="LOCATION" :showScaleInCopyrights="true" :ref="refMap">
            <!-- Add a map scheme layer -->
            <YMapDefaultSchemeLayer />
            <!-- Add clusterer data sources -->
            <YMapFeatureDataSource id="clusterer-source" />
            <!-- Add the layer for markers and the clusterer -->
            <YMapLayer source="clusterer-source" type="markers" :zIndex="1800" />
            <!-- In the clusterer props, we pass the previously declared functions for rendering markers and clusters,
            the clustering method, and an array of features -->
 
            <YMapClusterer :method="gridSizedMethod" v-if="isClusterer" :features="points">
              <template #marker="{feature}">
                <Marker :feature="feature" />
              </template>
              <template #cluster="{coordinates, features}">
                <Cluster :coordinates="coordinates" :features="features" />
              </template>
            </YMapClusterer>

            <!-- Add a custom clusterer change element to the map -->
            <YMapControls position="bottom">
              <YMapControl>
                <div class="clusterer-change">
                  <div class="clusterer-change__section">
                    <div class="clusterer-change__input__label">Point count:</div>
                    <input
                        type="number"
                        class="clusterer-change__input"
                        v-model="pointsCount"
                    />
                  </div>
                  <div class="clusterer-change__section">
                    <button type="button" class="clusterer-change__btn" @click="updatePoints">
                      Update points
                    </button>
                    <button type="button" class="clusterer-change__btn" @click="toggleClusterer">
                      Delete/Add Clusterer
                    </button>
                  </div>
                </div>
              </YMapControl>
            </YMapControls>
          </YMap>
        `
        });
        App.mount('#app');
      }
      main();
    </script>

    <style>
      html,
      body,
      #app {
        width: 100%;
        height: 100%;
        margin: 0;
        padding: 0;
        font-family: Arial, Helvetica, sans-serif;
      }

      .toolbar {
        position: absolute;
        z-index: 1000;
        top: 0;
        left: 0;
        display: flex;
        align-items: center;
        padding: 16px;
      }

      .toolbar a {
        padding: 16px;
      }
    </style>
    <link rel="stylesheet" href="./common.css" />
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
.clusterer-change {
  padding: 10px;
  border-radius: 10px;

  display: flex;
  flex-direction: column;
  align-items: center;

  gap: 10px;
}

.clusterer-change__section {
  display: flex;
  align-items: center;
  gap: 10px;
}

.clusterer-change__input__label {
  font-size: 18px;
}

.clusterer-change__input {
  max-width: 75px;
  height: 100%;

  font-size: 16px;
  text-align: center;

  border: 2px solid rgba(0, 122, 252, 0.6);
  border-radius: 5px;
  font-size: 16px;
  outline: none;
  transition: border-color 0.2s ease;
}

.clusterer-change__input:hover,
.clusterer-change__input:active {
  border-color: rgba(0, 122, 252, 0.9);
}

.clusterer-change__btn {
  border: none;
  cursor: pointer;

  padding: 7px 10px;

  color: rgb(255, 255, 255);
  font-size: 16px;

  background-color: rgba(0, 122, 252, 0.9);
  border-radius: 10px;
  transition: background-color 0.2s;
}

.clusterer-change__btn:hover {
  background-color: rgb(0, 110, 252);
}

.clusterer-change__btn:active {
  background-color: rgb(0, 122, 252);
}

.circle {
  position: relative;

  width: 40px;
  height: 40px;

  color: #7234c3;
  border: 2px solid currentColor;
  border-radius: 50%;
  background-color: rgba(255, 255, 255, 0.7);

  transform: translate(-50%, -50%);
}

.circle-content {
  position: absolute;
  top: 50%;
  left: 50%;

  display: flex;
  justify-content: center;
  align-items: center;

  width: 70%;
  height: 70%;

  border-radius: 50%;
  background-color: currentColor;

  transform: translate3d(-50%, -50%, 0);
}

.circle-text {
  font-size: 0.9em;

  color: #fff;
}

.pin {
  transform: translate(-50%, -100%);
}
import type {YMapLocationRequest, LngLatBounds, LngLat} from '@yandex/ymaps3-types';
import type {Feature} from '@yandex/ymaps3-clusterer';

/* Rectangle bounded by bottom-left and top-right coordinates
Inside it, we generate the first bundle of clusterer points */
export const BOUNDS: LngLatBounds = [
  [36.76, 56.5],
  [38.48, 54.98]
];
export const LOCATION: YMapLocationRequest = {
  bounds: BOUNDS as LngLatBounds, // starting position
  zoom: 9 // starting zoom
};

// Function for generating a pseudorandom number
const seed = (s: number) => () => {
  s = Math.sin(s) * 10000;
  return s - Math.floor(s);
};

const rnd = seed(10000); // () => Math.random()

// Generating random coordinates of a point [lng, lat] in a given boundary
const getRandomPointCoordinates = (bounds: LngLatBounds): LngLat => [
  bounds[0][0] + (bounds[1][0] - bounds[0][0]) * rnd(),
  bounds[1][1] + (bounds[0][1] - bounds[1][1]) * rnd()
];

// A function that creates an array with parameters for each clusterer random point
export const getRandomPoints = (count: number, bounds: LngLatBounds): Feature[] => {
  return Array.from({length: count}, (_, index) => ({
    type: 'Feature',
    id: index.toString(),
    geometry: {type: 'Point', coordinates: getRandomPointCoordinates(bounds)}
  }));
};