Components: Add MarkerClusterer to google-maps.

Created on 22 Nov 2019  路  17Comments  路  Source: angular/components

Add support of MarkerClusterer to google-maps.

P4 google-maps feature

Most helpful comment

I have MarkerClusterPlus implemented and working in an Angular 9 project. It is not hard, just

import {MarkerClustererOptions} from '@google/markerclustererplus';
import MarkerClusterer from '@google/markerclustererplus';

But, the main problem is that it takes a google.maps.Marker type and returns this type in an event, which makes it a bit of a pain to use the @angular/google-maps InfoWindow - since that takes a MapMarker type. It would be good to see MarkerClustererPlus integrated into @angular/google-maps just to solve that problem - as long as we don't lose anything else of course.

All 17 comments

This feature is definitely one that I'd like to implement. The main issue is addressing loading the separate marker clusterer library and see if there are types we can use, perhaps with DefinitelyTyped.

But it it's something I'd like to do in the future.

The google clustering library is now written in TS.

I would like to see a better solution. As I see Angular, it is best used for applications and in an application, I would see much more appropriate to be able to change the look of clusters.

Something like solution bellow might be the best

<google-map>
  <map-html-marker *mapMarkerClusterer="let cluster of allMarkers"></map-html-marker>
</google-map>

I have MarkerClusterPlus implemented and working in an Angular 9 project. It is not hard, just

import {MarkerClustererOptions} from '@google/markerclustererplus';
import MarkerClusterer from '@google/markerclustererplus';

But, the main problem is that it takes a google.maps.Marker type and returns this type in an event, which makes it a bit of a pain to use the @angular/google-maps InfoWindow - since that takes a MapMarker type. It would be good to see MarkerClustererPlus integrated into @angular/google-maps just to solve that problem - as long as we don't lose anything else of course.

Hi guys!
I built my own Component normally. Although I have to inject the inputs and outputs at the base of the application.

App Base - Calling MarkerClustererPlus

### App Base

`
import { Component, OnInit, ViewChild, Input, OnChanges, SimpleChanges, Output, EventEmitter, ElementRef, ViewChildren } from '@angular/core';
import { MapMarker, MapInfoWindow, GoogleMap } from '@angular/google-maps';
import { MarkerClustererPlusComponent } from '../marker-clusterer-plus/marker-clusterer-plus.component';
import MarkerClusterer from '@google/markerclustererplus';

@Component({
  selector: 'maine',
  template: `

      <google-map #googleMapRef 
        height="100vh"
        width="100%"
        [zoom]="zoom" 
        [center]="center" 
        (mapClick)="click($event);">

        <marker-clusterer-plus [googleMaps]="googleMapRef" (onLoaded)="onMcLoaded($event)" *ngFor="let cluster of markers" [icon_path]="cluster.instituicao.icon_path">

          <map-marker
            #markerElem 
            *ngFor="let marker of cluster.unidades"
            [position]="marker.position"
            [label]="marker.label"
            [title]="marker.title"
            [options]="marker.options"
            (mapClick)="openInfo(markerElem);"
          >
          </map-marker>

        </marker-clusterer-plus>

        <map-info-window>Teste</map-info-window>

      </google-map>

  `,
  styles: [
    `  
    `
  ]
})
export class AppComponent implements OnInit, OnChanges {

  @ViewChild(MapInfoWindow,{static:false}) infoWindow: MapInfoWindow;

  @ViewChild('googleMapRef')
  googleMaps : GoogleMap;

  @ViewChild('marker-clusterer-plus')
  markerClusterer : MarkerClustererPlusComponent;

  @Input()
  zoom:number = 8;

  @Input()
  center:google.maps.LatLngLiteral;

  @Input()
  markers: Array<any> = [];

  array_clusteres: Array<MarkerClusterer> = [];

  constructor(private element: ElementRef) { }

  ngOnInit(): void {

    navigator.geolocation.getCurrentPosition(position => {
      this.center = {
        lat: position.coords.latitude,
        lng: position.coords.longitude,
      }

      this.loadOptions();

      this.loadDataGeoJson();

    });

  }

  ngAfterViewInit(){
    console.log('MaineComponent ngAfterViewInit');
  }

  loadOptions(){

    this.googleMaps._googleMap.setOptions({
      styles: [
        {
          'stylers': [{'visibility': 'on'}]
        }, 
        {
          'featureType': 'landscape',
          'elementType': 'geometry',
          'stylers': [{'visibility': 'on'}, {'color': '#fcfcfc'}]
        }, 
        {
          'featureType': 'water',
          'elementType': 'geometry',
          'stylers': [{'visibility': 'on'}, {'color': '#bfd4ff'}]
        }
      ]
    });
  }

  loadDataGeoJson(){
this.googleMaps.data.loadGeoJson('https://raw.githubusercontent.com/codeforamerica/click_that_hood/master/public/data/rio-de-janeiro.geojson', {idPropertyName: 'STATE'});


    this.googleMaps.data.setStyle(styleFeature);


    let enableMouseClick = false;
    let enableMouseOver = false;
    let enableMouseOut = false;
    function styleFeature(feature) {

      var id = feature.getProperty('id');
      var name = feature.getProperty('name');
      var description = feature.getProperty('description');
      var state = feature.getProperty('state');
      var fillColor = '#131958';
      var fillOpacity = 0.3;
      var zIndex = 1;
      var clickable = 'true';
      var showRow = true; // determine whether to show this shape or not

      var options_default = {
          strokeColor: '#1e76ae', // #ffffff
          strokeOpacity: 0.8,
          strokeWeight: 1.5,
          fillColor: fillColor,
          fillOpacity: fillOpacity,
          zIndex: zIndex,
          visible: showRow
      };

      if (name != undefined) {

        if (state == 'click' && enableMouseClick) {}

        if (state == 'normal' && enableMouseOut) {
            feature.setProperty('withInfowindowOpen', false);
        }

        if (state == 'hover' && enableMouseOver) {
            options_default.strokeOpacity = 0.8;
            options_default.strokeColor = '#1e76ae';
            options_default.strokeWeight = 6;
            options_default.fillOpacity = 0.1;
            options_default.zIndex = -1;
        }
      }
      return options_default;
    }
      this.googleMaps.data.addListener('click', (e)=>{
       // set the hover state so the setStyle function can change the border
       e.feature.setProperty('state', 'click');
       console.log('CLICK REGION', e.feature);
    });

    this.googleMaps.data.addListener('mouseover', (e)=>{
      e.feature.setProperty('state', 'hover');
      e.feature.getProperty('NAME');
    });

    this.googleMaps.data.addListener('mouseout', (e)=>{
      e.feature.setProperty('state', 'normal');
    });

  }

  onMcLoaded(markerClusterer:MarkerClusterer){
    this.array_clusteres.push(markerClusterer);
    this.fitBoundsCluster(markerClusterer);
  }

  clearMarkerClusteres(){
    this.array_clusteres.forEach( (mc:MarkerClusterer)=>{
      mc.clearMarkers();
    });
  }

  fitBoundsCluster(markerClusterer:MarkerClusterer){

    let bounds = new google.maps.LatLngBounds();

    this.array_clusteres.forEach( (mc:MarkerClusterer)=>{
      mc.getMarkers().forEach( (m)=>{
        bounds.extend(m.getPosition());
      })
    })

    this.googleMaps.fitBounds(bounds);
  }


  openInfo(markerElem: MapMarker,content?:string){
    this.infoWindow.open(markerElem);
  }

  addMarker() {
    const plusLat = ((Math.random() - 0.5) * 10) / 10;
    const plusLng = ((Math.random() - 0.5) * 10) / 10;

    this.markers.push({
      position: {
        lat: this.center.lat + plusLat,
        lng: this.center.lng + plusLng,
      },
      label: {
        color: 'red',
        text: 'Marker label ' + (this.markers.length + 1),
      },
      title: 'Marker title ' + (this.markers.length + 1),
      options: { 
        // animation: google.maps.Animation.BOUNCE 
      },
    })
  }

  click(e: google.maps.MouseEvent) { console.log( JSON.stringify( e.latLng.toJSON() ) ); }
}
`

MarkerClustererPlus Component Wrapper

import { Component, Input, ContentChildren, QueryList, EventEmitter, Output } from '@angular/core';
import { GoogleMap, MapMarker } from '@angular/google-maps';
import MarkerClusterer, {ClusterIconStyle,MarkerClustererOptions} from '@google/markerclustererplus';
import { Cluster } from '@google/markerclustererplus/dist/cluster';

@Component({
  selector: 'marker-clusterer-plus',
  template: `<ng-content></ng-content>`,
  styles: [
  ]
})
export class MarkerClustererPlusComponent 
{
  mc: MarkerClusterer;

  @Input()
  googleMaps: GoogleMap;

  @ContentChildren("markerElem") 
  mapMarkers: QueryList<MapMarker>;

  @Input()
  title: string;

  @Input()
  icon_path: string;

  map: google.maps.Map;
  options: MarkerClustererOptions;
  styles: Array<ClusterIconStyle>;

  @Output()
  onLoaded: EventEmitter<any> = new EventEmitter<any>();

  constructor() { 

    this.options = {
      gridSize: 60,
      maxZoom: 20,
      averageCenter: false,
      zoomOnClick: true,
    };

    this.mc = new MarkerClusterer(null,[],this.options);
  }

  ngAfterViewInit(){
    this.init();
  }

  init(){

    this.map = this.googleMaps._googleMap;

    this.mapMarkers.forEach( (mapMarker:MapMarker)=>{
      this.mc.addMarker(mapMarker._marker);
    });
    this.mc.setTitle(this.title || "MarkerCluster");
    this.styles =[
      {
        url: this.icon_path || "http://image.flaticon.com/icons/svg/252/252025.svg",
        height: 52,
        width: 52,
        anchorText: [30,-10],
        textColor: '#fff',
        textSize: 10,
        fontWeight: "bold",
      }
    ];
    this.mc.setStyles(this.styles);
    this.mc.setMap(this.map);
    this.onLoaded.emit(this.mc);
    this.bindEvents();
  }
  bindEvents(){
    this.mc.addListener( 'mouseover' , (cluster:Cluster)=>{
      console.log(cluster);
    });
  }
}

@emiliojva

Thank you for your Code.

In the map-marker Element you iterate cluster.unidades. Where does this come from?

Thanks, Arndt

Is there any update on adding this as a feature to google-maps? This would be terrific!
Thanks!!

@emiliojva

Thank you for your Code.

In the map-marker Element you iterate cluster.unidades. Where does this come from?

Thanks, Arndt

I'm sorry for the delay in responding. 'cluster is just a variable that makes reference to an' array 'of 'unidades'(units in Portuguese) that I return from my api

Any news about this feature ?

Is it at least in the nearest future plans?

Hi guys!
I built my own Component normally. Although I have to inject the inputs and outputs at the base of the application.

App Base - Calling MarkerClustererPlus

### App Base

`
import { Component, OnInit, ViewChild, Input, OnChanges, SimpleChanges, Output, EventEmitter, ElementRef, ViewChildren } from '@angular/core';
import { MapMarker, MapInfoWindow, GoogleMap } from '@angular/google-maps';
import { MarkerClustererPlusComponent } from '../marker-clusterer-plus/marker-clusterer-plus.component';
import MarkerClusterer from '@google/markerclustererplus';

@Component({
  selector: 'maine',
  template: `

      <google-map #googleMapRef 
        height="100vh"
        width="100%"
        [zoom]="zoom" 
        [center]="center" 
        (mapClick)="click($event);">

        <marker-clusterer-plus [googleMaps]="googleMapRef" (onLoaded)="onMcLoaded($event)" *ngFor="let cluster of markers" [icon_path]="cluster.instituicao.icon_path">

          <map-marker
            #markerElem 
            *ngFor="let marker of cluster.unidades"
            [position]="marker.position"
            [label]="marker.label"
            [title]="marker.title"
            [options]="marker.options"
            (mapClick)="openInfo(markerElem);"
          >
          </map-marker>

        </marker-clusterer-plus>

        <map-info-window>Teste</map-info-window>

      </google-map>

  `,
  styles: [
    `  
    `
  ]
})
export class AppComponent implements OnInit, OnChanges {

  @ViewChild(MapInfoWindow,{static:false}) infoWindow: MapInfoWindow;

  @ViewChild('googleMapRef')
  googleMaps : GoogleMap;

  @ViewChild('marker-clusterer-plus')
  markerClusterer : MarkerClustererPlusComponent;

  @Input()
  zoom:number = 8;

  @Input()
  center:google.maps.LatLngLiteral;

  @Input()
  markers: Array<any> = [];

  array_clusteres: Array<MarkerClusterer> = [];

  constructor(private element: ElementRef) { }

  ngOnInit(): void {

    navigator.geolocation.getCurrentPosition(position => {
      this.center = {
        lat: position.coords.latitude,
        lng: position.coords.longitude,
      }

      this.loadOptions();

      this.loadDataGeoJson();

    });

  }

  ngAfterViewInit(){
    console.log('MaineComponent ngAfterViewInit');
  }

  loadOptions(){

    this.googleMaps._googleMap.setOptions({
      styles: [
        {
          'stylers': [{'visibility': 'on'}]
        }, 
        {
          'featureType': 'landscape',
          'elementType': 'geometry',
          'stylers': [{'visibility': 'on'}, {'color': '#fcfcfc'}]
        }, 
        {
          'featureType': 'water',
          'elementType': 'geometry',
          'stylers': [{'visibility': 'on'}, {'color': '#bfd4ff'}]
        }
      ]
    });
  }

  loadDataGeoJson(){
this.googleMaps.data.loadGeoJson('https://raw.githubusercontent.com/codeforamerica/click_that_hood/master/public/data/rio-de-janeiro.geojson', {idPropertyName: 'STATE'});


    this.googleMaps.data.setStyle(styleFeature);


    let enableMouseClick = false;
    let enableMouseOver = false;
    let enableMouseOut = false;
    function styleFeature(feature) {

      var id = feature.getProperty('id');
      var name = feature.getProperty('name');
      var description = feature.getProperty('description');
      var state = feature.getProperty('state');
      var fillColor = '#131958';
      var fillOpacity = 0.3;
      var zIndex = 1;
      var clickable = 'true';
      var showRow = true; // determine whether to show this shape or not

      var options_default = {
          strokeColor: '#1e76ae', // #ffffff
          strokeOpacity: 0.8,
          strokeWeight: 1.5,
          fillColor: fillColor,
          fillOpacity: fillOpacity,
          zIndex: zIndex,
          visible: showRow
      };

      if (name != undefined) {

        if (state == 'click' && enableMouseClick) {}

        if (state == 'normal' && enableMouseOut) {
            feature.setProperty('withInfowindowOpen', false);
        }

        if (state == 'hover' && enableMouseOver) {
            options_default.strokeOpacity = 0.8;
            options_default.strokeColor = '#1e76ae';
            options_default.strokeWeight = 6;
            options_default.fillOpacity = 0.1;
            options_default.zIndex = -1;
        }
      }
      return options_default;
    }
      this.googleMaps.data.addListener('click', (e)=>{
       // set the hover state so the setStyle function can change the border
       e.feature.setProperty('state', 'click');
       console.log('CLICK REGION', e.feature);
    });

    this.googleMaps.data.addListener('mouseover', (e)=>{
      e.feature.setProperty('state', 'hover');
      e.feature.getProperty('NAME');
    });

    this.googleMaps.data.addListener('mouseout', (e)=>{
      e.feature.setProperty('state', 'normal');
    });

  }

  onMcLoaded(markerClusterer:MarkerClusterer){
    this.array_clusteres.push(markerClusterer);
    this.fitBoundsCluster(markerClusterer);
  }

  clearMarkerClusteres(){
    this.array_clusteres.forEach( (mc:MarkerClusterer)=>{
      mc.clearMarkers();
    });
  }

  fitBoundsCluster(markerClusterer:MarkerClusterer){

    let bounds = new google.maps.LatLngBounds();

    this.array_clusteres.forEach( (mc:MarkerClusterer)=>{
      mc.getMarkers().forEach( (m)=>{
        bounds.extend(m.getPosition());
      })
    })

    this.googleMaps.fitBounds(bounds);
  }


  openInfo(markerElem: MapMarker,content?:string){
    this.infoWindow.open(markerElem);
  }

  addMarker() {
    const plusLat = ((Math.random() - 0.5) * 10) / 10;
    const plusLng = ((Math.random() - 0.5) * 10) / 10;

    this.markers.push({
      position: {
        lat: this.center.lat + plusLat,
        lng: this.center.lng + plusLng,
      },
      label: {
        color: 'red',
        text: 'Marker label ' + (this.markers.length + 1),
      },
      title: 'Marker title ' + (this.markers.length + 1),
      options: { 
        // animation: google.maps.Animation.BOUNCE 
      },
    })
  }

  click(e: google.maps.MouseEvent) { console.log( JSON.stringify( e.latLng.toJSON() ) ); }
}
`

MarkerClustererPlus Component Wrapper

import { Component, Input, ContentChildren, QueryList, EventEmitter, Output } from '@angular/core';
import { GoogleMap, MapMarker } from '@angular/google-maps';
import MarkerClusterer, {ClusterIconStyle,MarkerClustererOptions} from '@google/markerclustererplus';
import { Cluster } from '@google/markerclustererplus/dist/cluster';

@Component({
  selector: 'marker-clusterer-plus',
  template: `<ng-content></ng-content>`,
  styles: [
  ]
})
export class MarkerClustererPlusComponent 
{
  mc: MarkerClusterer;

  @Input()
  googleMaps: GoogleMap;

  @ContentChildren("markerElem") 
  mapMarkers: QueryList<MapMarker>;

  @Input()
  title: string;

  @Input()
  icon_path: string;

  map: google.maps.Map;
  options: MarkerClustererOptions;
  styles: Array<ClusterIconStyle>;

  @Output()
  onLoaded: EventEmitter<any> = new EventEmitter<any>();

  constructor() { 

    this.options = {
      gridSize: 60,
      maxZoom: 20,
      averageCenter: false,
      zoomOnClick: true,
    };

    this.mc = new MarkerClusterer(null,[],this.options);
  }

  ngAfterViewInit(){
    this.init();
  }

  init(){

    this.map = this.googleMaps._googleMap;

    this.mapMarkers.forEach( (mapMarker:MapMarker)=>{
      this.mc.addMarker(mapMarker._marker);
    });
    this.mc.setTitle(this.title || "MarkerCluster");
    this.styles =[
      {
        url: this.icon_path || "http://image.flaticon.com/icons/svg/252/252025.svg",
        height: 52,
        width: 52,
        anchorText: [30,-10],
        textColor: '#fff',
        textSize: 10,
        fontWeight: "bold",
      }
    ];
    this.mc.setStyles(this.styles);
    this.mc.setMap(this.map);
    this.onLoaded.emit(this.mc);
    this.bindEvents();
  }
  bindEvents(){
    this.mc.addListener( 'mouseover' , (cluster:Cluster)=>{
      console.log(cluster);
    });
  }
}

@emiliojva I just tried the same code in latest angular and am getting errors and map is not getting loaded.
Could you please help me ?

@pvkrijesh What is your errors ? can you create a stackblitz or something like that ?

@pvkrijesh What is your errors ? can you create a stackblitz or something like that ?

@ybarbaria I have created a stackblitz and please check it below.
https://stackblitz.com/edit/angular-9-google-maps-hpfnlu?file=src%2Fapp%2Fapp.component.ts.
I am expecting some clusters and on click of it should show the pins.
Hope this will help you to figure it out whats wrong.

@pvkrijesh
So in your app.component you have this array array_clusteresand this one markers but your never instantiate them or even add some data, your clusters won't be draw without data...

Hi @emiliojva,
Thank you for your code ! I tried to implemented it in my project, but I can't have what I want.
For now I'm only able to have the markers displayed, not the clusters...
I'm still not understanding the interest of the array_clusteres in your code as I can't see where it's used.
Do you have a working example with just few initial data ?

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

_This action has been performed automatically by a bot._

Was this page helpful?
0 / 5 - 0 ratings