infusion-code/angular-maps

View on GitHub
src/services/bing/bing-cluster.service.ts

Summary

Maintainability
F
3 days
Test Coverage
import { Injectable, NgZone } from '@angular/core';
import { IMarkerOptions } from '../../interfaces/imarker-options';
import { IPolygonOptions } from '../../interfaces/ipolygon-options';
import { IPolylineOptions } from '../../interfaces/ipolyline-options';
import { IClusterOptions } from '../../interfaces/icluster-options';
import { IMarkerIconInfo } from '../../interfaces/imarker-icon-info';
import { Marker } from '../../models/marker';
import { Polygon } from '../../models/polygon';
import { Polyline } from '../../models/polyline';
import { BingMarker } from '../../models/bing/bing-marker';
import { BingClusterLayer } from '../../models/bing/bing-cluster-layer';
import { Layer } from '../../models/layer';
import { MarkerTypeId } from '../../models/marker-type-id';
import { ClusterClickAction } from '../../models/cluster-click-action';
import { MapService } from '../map.service';
import { ClusterLayerDirective } from '../../components/cluster-layer';
import { ClusterService } from '../cluster.service';
import { BingLayerBase } from './bing-layer-base';
import { BingMapService } from './bing-map.service';
import { BingConversions } from './bing-conversions';

/**
 * Implements the {@link ClusterService} contract for a  Bing Maps V8 specific implementation.
 *
 * @export
 */
@Injectable()
export class BingClusterService extends BingLayerBase implements ClusterService {

    ///
    /// Constructor
    ///

    /**
     * Creates an instance of BingClusterService.
     * @param _mapService - Concrete {@link MapService} implementation for Bing Maps V8. An instance of {@link BingMapService}.
     * @param _zone - NgZone instance to provide zone aware promises.
     *
     * @memberof BingClusterService
     */
    constructor(_mapService: MapService, _zone: NgZone) {
        super(_mapService, _zone);
    }

    ///
    /// Public methods
    ///

    /**
     * Adds a layer to the map.
     *
     * @abstract
     * @param layer - ClusterLayerDirective component object.
     * Generally, MapLayer will be injected with an instance of the
     * LayerService and then self register on initialization.
     *
     * @memberof BingClusterService
     */
    public AddLayer(layer: ClusterLayerDirective): void {
        const options: IClusterOptions = {
            id: layer.Id,
            visible: layer.Visible,
            clusteringEnabled: layer.ClusteringEnabled,
            placementMode: layer.ClusterPlacementMode
        };
        if (layer.GridSize) { options.gridSize = layer.GridSize; }
        if (layer.LayerOffset) { options.layerOffset = layer.LayerOffset; }
        if (layer.ZIndex) { options.zIndex = layer.ZIndex; }
        if (layer.IconInfo) {
            options.clusteredPinCallback = (pin: Microsoft.Maps.ClusterPushpin) => { this.CreateClusterPushPin(pin, layer); };
        }
        if (layer.CustomMarkerCallback) {
            options.clusteredPinCallback = (pin: Microsoft.Maps.ClusterPushpin) => { this.CreateCustomClusterPushPin(pin, layer); };
        }
        if (layer.SpiderClusterOptions) { options.spiderClusterOptions = layer.SpiderClusterOptions; }

        const layerPromise: Promise<Layer> = this._mapService.CreateClusterLayer(options);
        (<BingMapService>this._mapService).MapPromise.then(m => {
            Microsoft.Maps.Events.addHandler(m, 'viewchangeend', (e) => {
                if (layer.ClusteringEnabled && m.getZoom() === 19) {
                    layerPromise.then((l: BingClusterLayer) => {
                        l.SetOptions({ id: layer.Id, clusteringEnabled: false });
                    });
                }
                if (layer.ClusteringEnabled && m.getZoom() < 19) {
                    layerPromise.then((l: BingClusterLayer) => {
                        if (!l.GetOptions().clusteringEnabled) {
                            l.SetOptions({ id: layer.Id, clusteringEnabled: true });
                        }
                    });
                }
            });
        });
        this._layers.set(layer.Id, layerPromise);
    }

    /**
     * Adds a polygon to the layer.
     *
     * @abstract
     * @param layer - The id of the layer to which to add the polygon.
     * @param options - Polygon options defining the polygon.
     * @returns - A promise that when fullfilled contains the an instance of the Polygon model.
     *
     * @memberof BingClusterService
     */
    public CreatePolygon(layer: number, options: IPolygonOptions): Promise<Polygon> {
        throw (new Error('Polygons are not supported in clustering layers. You can only use markers.'));
    }

    /**
     * Creates an array of unbound polygons. Use this method to create arrays of polygons to be used in bulk
     * operations.
     *
     * @param layer - The id of the layer to which to add the polygon.
     * @param options - Polygon options defining the polygons.
     * @returns - A promise that when fullfilled contains the an arrays of the Polygon models.
     *
     * @memberof BingClusterService
     */
    public CreatePolygons(layer: number, options: Array<IPolygonOptions>): Promise<Array<Polygon>> {
        throw (new Error('Polygons are not supported in clustering layers. You can only use markers.'));
    }

    /**
     * Adds a polyline to the layer.
     *
     * @abstract
     * @param layer - The id of the layer to which to add the line.
     * @param options - Polyline options defining the line.
     * @returns - A promise that when fullfilled contains the an instance of the Polyline (or an array
     * of polygons for complex paths) model.
     *
     * @memberof BingClusterService
     */
    public CreatePolyline(layer: number, options: IPolylineOptions): Promise<Polyline|Array<Polyline>> {
        throw (new Error('Polylines are not supported in clustering layers. You can only use markers.'));
    }

    /**
     * Creates an array of unbound polylines. Use this method to create arrays of polylines to be used in bulk
     * operations.
     *
     * @param layer - The id of the layer to which to add the polylines.
     * @param options - Polyline options defining the polylines.
     * @returns - A promise that when fullfilled contains the an arrays of the Polyline models.
     *
     * @memberof BingClusterService
     */
    public CreatePolylines(layer: number, options: Array<IPolylineOptions>): Promise<Array<Polyline|Array<Polyline>>> {
        throw (new Error('Polylines are not supported in clustering layers. You can only use markers.'));
    }

    /**
     * Start to actually cluster the entities in a cluster layer. This method should be called after the initial set of entities
     * have been added to the cluster. This method is used for performance reasons as adding an entitiy will recalculate all clusters.
     * As such, StopClustering should be called before adding many entities and StartClustering should be called once adding is
     * complete to recalculate the clusters.
     *
     * @param layer - ClusterLayerDirective component object for which to retrieve the layer.
     *
     * @memberof BingClusterService
     */
    public StartClustering(layer: ClusterLayerDirective): Promise<void> {
        const l = this._layers.get(layer.Id);
        if (l == null) {
            return Promise.resolve();
        }
        return l.then((l1: BingClusterLayer) => {
            return this._zone.run(() => {
                l1.StartClustering();
            });
        });
    }

    /**
     * Stop to actually cluster the entities in a cluster layer.
     * This method is used for performance reasons as adding an entitiy will recalculate all clusters.
     * As such, StopClustering should be called before adding many entities and StartClustering should be called once adding is
     * complete to recalculate the clusters.
     *
     * @param layer - ClusterLayerDirective component object for which to retrieve the layer.
     *
     * @memberof BingClusterService
     */
    public StopClustering(layer: ClusterLayerDirective): Promise<void> {
        const l = this._layers.get(layer.Id);
        if (l == null) {
            return Promise.resolve();
        }
        return l.then((l1: BingClusterLayer) => {
            return this._zone.run(() => {
                l1.StopClustering();
            });
        });
    }

    ///
    /// Private methods
    ///

    /**
     * Creates the default cluster pushpin as a callback from BingMaps when clustering occurs. The {@link ClusterLayerDirective} model
     * can provide an IconInfo property that would govern the apparenace of the pin. This method will assign the same pin to all
     * clusters in the layer.
     *
     * @param cluster - The cluster for which to create the pushpin.
     * @param layer - The {@link ClusterLayerDirective} component representing the layer.
     *
     * @memberof BingClusterService
     */
    private CreateClusterPushPin(cluster: Microsoft.Maps.ClusterPushpin, layer: ClusterLayerDirective): void {
        this._layers.get(layer.Id).then((l: BingClusterLayer) => {
            if (layer.IconInfo) {
                const o: Microsoft.Maps.IPushpinOptions = {};
                const payload: (ico: string, info: IMarkerIconInfo) => void = (ico, info) => {
                        o.icon = ico;
                        o.anchor = new Microsoft.Maps.Point(
                            (info.size && info.markerOffsetRatio) ? (info.size.width * info.markerOffsetRatio.x) : 0,
                            (info.size && info.markerOffsetRatio) ? (info.size.height * info.markerOffsetRatio.y) : 0
                        );
                        cluster.setOptions(o);
                };
                const icon: string|Promise<{icon: string, iconInfo: IMarkerIconInfo}> = Marker.CreateMarker(layer.IconInfo);
                if (typeof(icon) === 'string') {
                    payload(icon, layer.IconInfo);
                }
                else {
                    icon.then(x => {
                        payload(x.icon, x.iconInfo);
                    });
                }
            }
            if (layer.ClusterClickAction === ClusterClickAction.ZoomIntoCluster) {
                Microsoft.Maps.Events.addHandler(cluster, 'click', (e: Microsoft.Maps.IMouseEventArgs) => this.ZoomIntoCluster(e));
            }
            if (layer.ClusterClickAction === ClusterClickAction.Spider) {
                Microsoft.Maps.Events.addHandler(cluster, 'dblclick', (e: Microsoft.Maps.IMouseEventArgs) => this.ZoomIntoCluster(e));
                l.InitializeSpiderClusterSupport();
            }
        });
    }

    /**
     * Provides a hook for consumers to provide a custom function to create cluster bins for a cluster. This is particuarily useful
     * in situation where the pin should differ to represent information about the pins in the cluster.
     *
     * @param cluster - The cluster for which to create the pushpin.
     * @param layer - The {@link ClusterLayerDirective} component
     * representing the layer. Set the {@link ClusterLayerDirective.CustomMarkerCallback}
     * property to define the callback generating the pin.
     *
     * @memberof BingClusterService
     */
    private CreateCustomClusterPushPin(cluster: Microsoft.Maps.ClusterPushpin, layer: ClusterLayerDirective): void {
        this._layers.get(layer.Id).then((l: BingClusterLayer) => {
            // assemble markers for callback
            const m: Array<Marker> = new Array<Marker>();
            cluster.containedPushpins.forEach(p => {
                const marker: Marker = l.GetMarkerFromBingMarker(p);
                if (marker) { m.push(marker); }
            });
            const iconInfo: IMarkerIconInfo = { markerType: MarkerTypeId.None };
            const o: Microsoft.Maps.IPushpinOptions = {};
            o.icon = layer.CustomMarkerCallback(m, iconInfo);
            if (o.icon !== '') {
                o.anchor = new Microsoft.Maps.Point(
                    (iconInfo.size && iconInfo.markerOffsetRatio) ? (iconInfo.size.width * iconInfo.markerOffsetRatio.x) : 0,
                    (iconInfo.size && iconInfo.markerOffsetRatio) ? (iconInfo.size.height * iconInfo.markerOffsetRatio.y) : 0
                );
                if (iconInfo.textOffset) { o.textOffset = new Microsoft.Maps.Point(iconInfo.textOffset.x, iconInfo.textOffset.y); }
                cluster.setOptions(o);
            }
            if (layer.ClusterClickAction === ClusterClickAction.ZoomIntoCluster) {
                Microsoft.Maps.Events.addHandler(cluster, 'click', (e: Microsoft.Maps.IMouseEventArgs) => this.ZoomIntoCluster(e));
            }
            if (layer.ClusterClickAction === ClusterClickAction.Spider) {
                Microsoft.Maps.Events.addHandler(cluster, 'dblclick', (e: Microsoft.Maps.IMouseEventArgs) => this.ZoomIntoCluster(e));
                l.InitializeSpiderClusterSupport();
            }
        });
    }

    /**
     * Zooms into the cluster on click so that the members of the cluster comfortable fit into the zommed area.
     *
     * @param e - Mouse Event.
     *
     * @memberof BingClusterService
     */
    private ZoomIntoCluster(e: Microsoft.Maps.IMouseEventArgs): void {
        const pin: Microsoft.Maps.ClusterPushpin = <Microsoft.Maps.ClusterPushpin>e.target;
        if (pin && pin.containedPushpins) {
            let bounds: Microsoft.Maps.LocationRect;
            const locs: Array<Microsoft.Maps.Location> = new Array<Microsoft.Maps.Location>();
            pin.containedPushpins.forEach(p => locs.push(p.getLocation()));
            bounds = Microsoft.Maps.LocationRect.fromLocations(locs);

            // Zoom into the bounding box of the cluster.
            // Add a padding to compensate for the pixel area of the pushpins.
            (<BingMapService>this._mapService).MapPromise.then((m: Microsoft.Maps.Map) => {
                m.setView({ bounds: bounds, padding: 75 });
            });
        }
    }

}