yandex-maps-unofficial / vue-yandex-map

Yandex Maps Component for VueJS
MIT License
359 stars 103 forks source link

Навешивание клика на метки при обновлении списка меток #196

Closed constairs closed 4 years ago

constairs commented 4 years ago

Доброго времени! Я получаю маркеры на карту каждый раз при изменении bounds - мне приходит список меток, входящих в текущую область карты. Проблема в навешивании событий на маркеры. Я делаю это так: const objectManager = map.geoObjects.get(0); objectManager.objects.events.add('click', this.onClick); Это выполняется каждый раз при изменении списка меток. Но иногда оно фейлится с ошибкой: Cannot read property 'objects' of undefined at VueComponent.initClickEvents (.6697a674.js:755) at VueComponent._callee$ (.6697a674.js:497) at tryCatch (commons.app.js:9132) at Generator.invoke [as _invoke] (commons.app.js:9366) at Generator.prototype.<computed> [as next] (commons.app.js:9184) at asyncGeneratorStep (commons.app.js:53) at _next (commons.app.js:75) Также я фильрую маркеры, и при их переключении также возникают такие проблемы. Есть ли какой-то другой способ навешивать события на маркеры? Как всё это дело синхронизиовать с получением меток? Спасибо!

constairs commented 4 years ago
<template>
  <div class="appAbstractMap">
    <YandexMap
      :zoom.sync="zoom"
      :coords.sync="coords"
      :show-all-markers="showAllMarkers"
      :controls="$options.YandexMapControls"
      :use-object-manager="true"
      :object-manager-clusterize="true"
      :cluster-options="$options.YandexMapClusterOptions"
      @map-was-initialized="getMapInstance">
      <template v-for="placemark in placemarks">
        <YandexMapMarker
          :key="generateKey(placemark) || '1'"
          :coords="getCoords(placemark)"
          :options="getMarkerStyle(placemark.type)"
          :hint-content="renderHintComponent(placemark)"
          :marker-id="generateKey(placemark) || '1'"
          :properties="{
            type: placemark.type || null,
            price: placemark.minPrice || null,
            priceAnalog: placemark.counterpartMinPrice || null,
          }"
          cluster-name="0"
          marker-type="placemark"
        />
      </template>
    </YandexMap>
    <slot
      name="filter"
    />
  </div>
</template>

<script>
import Vue from 'vue';
import {
  isNil,
  isEmpty,
  debounce,
} from 'lodash-es';
import { yandexMap, ymapMarker } from 'vue-yandex-maps';
import createHintEntity from '~/assets/js/helpers/hint/createHintEntity';
import generateMapMarkerKey from '~/assets/js/helpers/generate-map-marker-key';

const mapMarkerSvgOriginals = "data:image/svg+xml,%3Csvg width='22' height='33' viewBox='0 0 22 33' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.6568 5.39236C13.5329 2.20254 8.46756 2.20254 5.34315 5.39236C2.52815 8.26671 2.21127 13.6788 4.60023 16.9311L11 27L17.3998 16.9311C19.7887 13.6788 19.4718 8.26671 16.6568 5.39236ZM11.0779 13.7853C9.61935 13.7853 8.43719 12.5782 8.43719 11.0889C8.43719 9.59962 9.61935 8.39254 11.0779 8.39254C12.5365 8.39254 13.7186 9.59962 13.7186 11.0889C13.7186 12.5782 12.5365 13.7853 11.0779 13.7853Z' fill='%2314A41A'/%3E%3Cpath d='M4.27156 4.34275L4.27149 4.34283C2.56189 6.08846 1.67316 8.52682 1.52313 10.9264C1.37416 13.3091 1.94254 15.8214 3.36011 17.7763L9.73407 27.8046L11 29.7963L12.2659 27.8046L18.6399 17.7763C20.0575 15.8214 20.6258 13.3091 20.4769 10.9264C20.3268 8.52682 19.4381 6.08846 17.7285 4.34283L16.6568 5.39236L17.7285 4.34283C14.0163 0.552349 7.98412 0.552459 4.27156 4.34275ZM11.0779 12.2853C10.4771 12.2853 9.93719 11.7795 9.93719 11.0889C9.93719 10.3984 10.4771 9.89254 11.0779 9.89254C11.6787 9.89254 12.2186 10.3984 12.2186 11.0889C12.2186 11.7795 11.6787 12.2853 11.0779 12.2853Z' stroke='white' stroke-opacity='0.8' stroke-width='3'/%3E%3C/svg%3E%0A";
const mapMarkerSvgAnalogs = "data:image/svg+xml,%3Csvg width='22' height='33' viewBox='0 0 22 33' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M16.6568 5.39236C13.5329 2.20254 8.46756 2.20254 5.34315 5.39236C2.52815 8.26671 2.21127 13.6788 4.60023 16.9311L11 27L17.3998 16.9311C19.7887 13.6788 19.4718 8.26671 16.6568 5.39236ZM11.0779 13.7853C9.61935 13.7853 8.43719 12.5782 8.43719 11.0889C8.43719 9.59962 9.61935 8.39254 11.0779 8.39254C12.5365 8.39254 13.7186 9.59962 13.7186 11.0889C13.7186 12.5782 12.5365 13.7853 11.0779 13.7853Z' fill='%23F75A52'/%3E%3Cpath d='M4.27156 4.34275L4.27149 4.34283C2.56189 6.08846 1.67316 8.52682 1.52313 10.9264C1.37416 13.3091 1.94254 15.8214 3.36011 17.7763L9.73407 27.8046L11 29.7963L12.2659 27.8046L18.6399 17.7763C20.0575 15.8214 20.6258 13.3091 20.4769 10.9264C20.3268 8.52682 19.4381 6.08846 17.7285 4.34283L16.6568 5.39236L17.7285 4.34283C14.0163 0.552349 7.98412 0.552459 4.27156 4.34275ZM11.0779 12.2853C10.4771 12.2853 9.93719 11.7795 9.93719 11.0889C9.93719 10.3984 10.4771 9.89254 11.0779 9.89254C11.6787 9.89254 12.2186 10.3984 12.2186 11.0889C12.2186 11.7795 11.6787 12.2853 11.0779 12.2853Z' stroke='white' stroke-opacity='0.8' stroke-width='3'/%3E%3C/svg%3E%0A";
const mapMarkerSvgBoth = "data:image/svg+xml,%3Csvg width='22' height='33' viewBox='0 0 22 33' xmlns='http://www.w3.org/2000/svg'%3E%3ClinearGradient id='linear-gradient'%3E%3Cstop offset='0%25' stop-color='rgba(20,164,26,1)'/%3E%3Cstop offset='50%25' stop-color='rgba(20,164,26,1)'/%3E%3Cstop offset='50%25' stop-color='rgba(247,90,82,1)'/%3E%3Cstop offset='100%25' stop-color='rgba(247,90,82,1)'/%3E%3C/linearGradient%3E%3Cpath d='M16.6568 5.39236C13.5329 2.20254 8.46756 2.20254 5.34315 5.39236C2.52815 8.26671 2.21127 13.6788 4.60023 16.9311L11 27L17.3998 16.9311C19.7887 13.6788 19.4718 8.26671 16.6568 5.39236ZM11.0779 13.7853C9.61935 13.7853 8.43719 12.5782 8.43719 11.0889C8.43719 9.59962 9.61935 8.39254 11.0779 8.39254C12.5365 8.39254 13.7186 9.59962 13.7186 11.0889C13.7186 12.5782 12.5365 13.7853 11.0779 13.7853Z'/%3E%3Cpath fill='url(%23linear-gradient)' d='M4.27156 4.34275L4.27149 4.34283C2.56189 6.08846 1.67316 8.52682 1.52313 10.9264C1.37416 13.3091 1.94254 15.8214 3.36011 17.7763L9.73407 27.8046L11 29.7963L12.2659 27.8046L18.6399 17.7763C20.0575 15.8214 20.6258 13.3091 20.4769 10.9264C20.3268 8.52682 19.4381 6.08846 17.7285 4.34283L16.6568 5.39236L17.7285 4.34283C14.0163 0.552349 7.98412 0.552459 4.27156 4.34275ZM11.0779 12.2853C10.4771 12.2853 9.93719 11.7795 9.93719 11.0889C9.93719 10.3984 10.4771 9.89254 11.0779 9.89254C11.6787 9.89254 12.2186 10.3984 12.2186 11.0889C12.2186 11.7795 11.6787 12.2853 11.0779 12.2853Z' stroke='white' stroke-opacity='0.8' stroke-width='3'/%3E%3C/svg%3E";

export default {
  name: 'AppAbstractMap',
  components: {
    YandexMap: yandexMap,
    YandexMapMarker: ymapMarker,
  },
  props: {
    placemarks: {
      type: Array,
      default() { return []; },
    },
    activeEntityIndex: {
      type: Number,
      default: 0,
    },
    filters: {
      type: Array,
      default() { return []; },
    },
    canShowMobile: {
      type: Boolean,
      default: false,
    },
    balloonConfiguration: {
      type: Object,
      default() { return {}; },
    },
    hintConfiguration: {
      type: Object,
      default() { return {}; },
    },
    mapZoom: {
      type: Number,
      default() { return 9; },
    },
    centerCoords: {
      type: Array,
      default() { return [56.098943, 38.706567]; },
    },
    showAllMarkers: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    // центральные карты текущего города и зум
    const { centerCoords, mapZoom } = this;
    return {
      currentMap: null,
      isInit: false,
      coords: centerCoords,
      zoom: mapZoom,
      KEY: 1,
      balloonOpened: false,
      balloonPositionMap: [0, 0],
      balloonDirection: 'default',
      activeMarkerId: null,
    };
  },
  computed: {
    // задержка ф-ии при перемещении карты
    debouncedFetchNewMarkers(bounds, initial) {
      return debounce(this.fetchNewMarkers, 300, { bounds, initial });
    },
  },
  watch: {
    placemarks: {
      // вот здесь я отлавливаю, что пришли новые маркеры и пытаюсь заинитить на них клики
      async handler(changes) {
        if (this.KEY < 2) {
          this.KEY += 1;
        }
        if (changes.length !== 0) {
          this.KEY = 1;
          this.applyMarkerFilters(this.filters);
          await this.$nextTick();
          if (this.currentMap) {
            this.initClickEvents(this.currentMap);
          }
        }
      },
      deep: true,
    },
    centerCoords: {
      handler(newCoords) {
        this.coords = newCoords;
        if (!isNil(this.currentMap)) {
          this.$nextTick(() => {
            this.debouncedFetchNewMarkers(this.currentMap.getBounds());
          });
        }
      },
    },
    mapZoom: {
      handler(newZoom) {
        this.zoom = newZoom;
        if (!isNil(this.currentMap)) {
          this.$nextTick(() => {
            this.debouncedFetchNewMarkers(this.currentMap.getBounds());
          });
        }
      },
    },
    filters: {
      handler(newFilters) {
        this.applyMarkerFilters(newFilters);
      },
    },
  },
  created() {
    this.$options.YandexMapClusterOptions = {
      0: {
        clusterIconLayout: 'default#pieChart',
        clusterDisableClickZoom: false,
        clusterOpenBalloonOnClick: false,
        minClusterSize: 3,
        clusterHasHint: true,
      },
    };
    this.$options.YandexMapControls = [
      'zoomControl',
      'geolocationControl',
    ];
    this.$options.getYandexMapMarkerIconSetting = {
      hideIconOnBalloonOpen: false,
      iconLayout: 'default#image',
      iconImageSize: [28, 40],
      iconImageOffset: [-14, -20],
      iconShadow: true,
      zIndex: 110,
    };
    this.$options.YandexMapMarkerTypeA = {
      ...this.$options.getYandexMapMarkerIconSetting,
      iconImageHref: mapMarkerSvgOriginals,
      iconColor: '#14A41A',
    };
    this.$options.YandexMapMarkerTypeB = {
      ...this.$options.getYandexMapMarkerIconSetting,
      iconImageHref: mapMarkerSvgAnalogs,
      iconColor: '#ed4543',
    };
    this.$options.YandexMapMarkerTypeC = {
      ...this.$options.getYandexMapMarkerIconSetting,
      iconImageHref: mapMarkerSvgBoth,
      iconColor: '#14A41A',
    };
    this.$options.YandexMapMarkerDefault = {
      preset: 'islands#icon',
      hideIconOnBalloonOpen: false,
      iconColor: '#ed4543',
      zIndex: 110,
    };
  },
  methods: {
    createHint: createHintEntity,
    async getMapInstance(map) {
      if (!isNil(map)) {
        try {
          // буре текущую область, чтобы получить первые маркеры
          // есть ли спопсоб получать их сразу до инициализации карты?
          const bounds = map.getBounds();
          this.debouncedFetchNewMarkers(bounds, true);

          this.currentMap = map;
          this.currentMap.options.set('placemark');
          this.currentMap.options.set('yandexMapDisablePoiInteractivity', true);
          this.currentMap.options.set('minZoom', 3);
          this.initMapEvents(map);

          if (this.filters.length) {
            this.applyMarkerFilters(this.filters);
          }

          this.isInit = true;
        } catch (error) {
          console.log(error);
          this.isInit = true;
        }
      }
    },
    fetchNewMarkers(bounds, isInitial) {
      this.$emit('fetch-new-markers', {
        bounds,
        isInitial,
      });
    },
    onDragEnd(e) {
      const prevZoom = e.get('oldZoom');
      const newZoom = e.get('newZoom');
      const oldCenter = e.get('oldCenter');
      const newCenter = e.get('newCenter');
      const currentBounds = e.get('newBounds');
      const deltaX = Math.abs(oldCenter[0] - newCenter[0]);
      const deltaY = Math.abs(oldCenter[1] - newCenter[1]);
      const leftLat = currentBounds[0][1];
      const rightLat = currentBounds[1][1];
      const boundsToFetch = [
        [
          currentBounds[0][0],
          (leftLat < -179.999) || (leftLat > 1) ? -179.999 : leftLat,
        ],
        [
          currentBounds[1][0],
          (rightLat > 179.999 || rightLat < -1) ? 179.999 : rightLat,
        ],
      ];

      this.$nextTick(() => {
        const largeScroll = deltaX > 0.25 || deltaY > 0.25;

        if (newZoom <= prevZoom && largeScroll) {
          this.debouncedFetchNewMarkers(boundsToFetch);
        }
      });
    },
    onClick(e) {
      const target = e.get('objectId');
      const activeEntityIndex = this.placemarks.findIndex(placemark => this.generateKey(placemark) === target);
      if (this.canShowMobile) {
        const center = this.toGlobalPixels(this.getCoords(this.placemarks[activeEntityIndex]), 16);
        const mobileOffset = window.screen.availHeight / 4;
        this.currentMap.setCenter(this.toCoords([center[0], center[1] - mobileOffset], 16));
      } else {
        this.currentMap.setCenter(this.getCoords(this.placemarks[activeEntityIndex]));
      }
      this.$emit('select-entity', [
        activeEntityIndex,
        this.placemarks[activeEntityIndex],
      ]);
      this.activeMarkerId = target;
    },
    compileTemplate(component, propsData) {
      const Extended = Vue.extend(component);
      let Instance = new Extended({
        route: this.$route,
        store: this.$store,
        propsData,
      });
      Instance.$mount();
      const html = Instance.$el && Instance.$el.outerHTML;
      Instance = null;
      return html;
    },
    getRenderedComponent(component, entity, props) {
      if (!isEmpty(component) || !isNil(component)) {
        return this.compileTemplate(component, { entity, ...props });
      }
      return '';
    },
    renderHintComponent(entity) {
      return this.getRenderedComponent(
        this.hintConfiguration.template,
        entity,
      );
    },
    getCoords(entity) {
      const coords = [];
      if (entity) {
        entity.coord_lat && coords.push(entity.coord_lat);
        entity.coord_lng && coords.push(entity.coord_lng);
        entity.lat && coords.push(entity.lat);
        entity.lng && coords.push(entity.lng);
        entity.coordLat && coords.push(entity.coordLat);
        entity.coordLng && coords.push(entity.coordLng);
      } else {
        coords.push(0);
        coords.push(0);
      }
      return coords;
    },
    getMarkerStyle(type) {
      if (!type || (type === 'both')) {
        return this.$options.YandexMapMarkerTypeC;
      }

      return type === 'originals' ? this.$options.YandexMapMarkerTypeA : this.$options.YandexMapMarkerTypeB;
    },
    toGlobalPixels(coords, zoom = this.currentMap.getZoom()) {
      const projection = this.currentMap.options.get('projection');
      return projection.toGlobalPixels(coords, zoom);
    },
    toCoords(pixels, zoom = this.currentMap.getZoom()) {
      const projection = this.currentMap.options.get('projection');
      return projection.fromGlobalPixels(pixels, zoom);
    },
    getMarkerPixelPosition(markerId, zoom = this.currentMap.getZoom()) {
      const objectManager = this.currentMap.geoObjects.get(0);
      const currentMarker = objectManager.objects.getById(markerId);
      const gPixels = this.toGlobalPixels(currentMarker.geometry.coordinates, zoom);

      return this.currentMap.panes.get('controls').toClientPixels(gPixels);
    },
    generateKey: generateMapMarkerKey,
    initClickEvents(map) {
      this.KEY += 1;
      try {
        const objectManager = map.geoObjects.get(0);
        objectManager.objects.events.add('click', this.onClick);
        this.KEY = 1;
      } catch (e) {
        if (this.KEY < 2) {
          this.initClickEvents(map);
        }
        console.log(e);
      }
    },
    initMapEvents(map) {
      map.events.add('boundschange', this.onDragEnd);
    },
    applyMarkerFilters(filters) {
      // фильрация маркеров с помощью object mananger
      // (есть ещё варииант, где фильруется в родительском компонените)
      const isBoth = filters.length === 2;
      const isOriginals = filters.find(item => item === 'originals');
      const isAnalogs = filters.find(item => item === 'analogs');

      const objectManager = this.currentMap.geoObjects.get(0);
      if (objectManager) {
        objectManager.setFilter((object) => {
          if (isBoth) {
            return object.properties.type === 'both'
            || object.properties.type === 'analogs'
            || object.properties.type === 'originals';
          }

          if (isOriginals) {
            return object.properties.type === 'originals';
          }

          if (isAnalogs) {
            return object.properties.type === 'analogs';
          }

          return false;
        });
      }
    },
  },
};
</script>

<style lang="postcss">
  @import '~/assets/css/components/map/hint.css';
  @import '~/assets/css/components/map/balloon.css';
  .appAbstractMap {
    position: relative;
  }
  .appAbstractMap,
  .ymap-container {
    width: inherit;
    height: 20rem;
  }

  .ymaps-2-1-74-balloon__content {
    padding: 0 !important;
  }

  .ymaps-2-1-74-balloon__close + .ymaps-2-1-74-balloon__content {
    margin-right: 0!important;
  }

  .ymaps-2-1-74-balloon__tail::after {
    background-color: var(--bg-lightest) !important;
  }

  @media screen and (max-width: 980px) {
    .appAbstractMap {
      height: inherit;
    }

    .ymap-container {
      height: inherit;
    }

    .ymaps-2-1-74-balloon,
    .ymaps-2-1-74-hint-overlay {
      display: none;
    }
  }
</style>
constairs commented 4 years ago

Прошу прошения, не могу прикрепить какой-либо пример на codepen или подобных сервисах, т.к. логика получия меток слишком сложна и завязана на куче компонентов. Но возможно код самого компонента с картой будет полезен.

PNKBizz commented 4 years ago

@constairs упростите код до минимума, который описывает проблему и выложите на codepen, пожалуйста