src/models/bing/bing-cluster-layer.ts
import { IClusterOptions } from '../../interfaces/icluster-options';
import { ISpiderClusterOptions } from '../../interfaces/ispider-cluster-options';
import { BingConversions } from '../../services/bing/bing-conversions';
import { BingMapService } from '../../services/bing/bing-map.service';
import { MapService } from '../../services/map.service';
import { Layer } from '../layer';
import { Marker } from '../marker';
import { InfoWindow } from '../info-window';
import { BingSpiderClusterMarker } from './bing-spider-cluster-marker';
import { BingMarker } from './bing-marker';
/**
* Concrete implementation of a clustering layer for the Bing Map Provider.
*
* @export
*/
export class BingClusterLayer implements Layer {
///
/// Field declarations
///
private _isClustering = true;
private _markers: Array<Marker> = new Array<Marker>();
private _markerLookup: Map<Microsoft.Maps.Pushpin, Marker> = new Map<Microsoft.Maps.Pushpin, Marker>();
private _pendingMarkers: Array<Marker> = new Array<Marker>();
private _spiderMarkers: Array<BingSpiderClusterMarker> = new Array<BingSpiderClusterMarker>();
private _spiderMarkerLookup: Map<Microsoft.Maps.Pushpin, BingSpiderClusterMarker> =
new Map<Microsoft.Maps.Pushpin, BingSpiderClusterMarker>();
private _useSpiderCluster = false;
private _mapclicks = 0;
private _spiderLayer: Microsoft.Maps.Layer;
private _events: Array<Microsoft.Maps.IHandlerId> = new Array<Microsoft.Maps.IHandlerId>();
private _currentZoom = 0;
private _spiderOptions: ISpiderClusterOptions = {
circleSpiralSwitchover: 9,
collapseClusterOnMapChange: false,
collapseClusterOnNthClick: 1,
invokeClickOnHover: true,
minCircleLength: 60,
minSpiralAngleSeperation: 25,
spiralDistanceFactor: 5,
stickStyle: {
strokeColor: 'black',
strokeThickness: 2
},
stickHoverStyle: { strokeColor: 'red' },
markerSelected: null,
markerUnSelected: null
};
private _currentCluster: Microsoft.Maps.ClusterPushpin = null;
///
/// Property definitions
///
/**
* Get the native primitive underneath the abstraction layer.
*
* @returns Microsoft.Maps.ClusterLayer.
*
* @memberof BingClusterLayer
*/
public get NativePrimitve(): any {
return this._layer;
}
///
/// Constructor
///
/**
* Creates a new instance of the BingClusterLayer class.
*
* @param _layer Microsoft.Maps.ClusterLayer. Native Bing Cluster Layer supporting the cluster layer.
* @param _maps MapService. MapService implementation to leverage for the layer.
*
* @memberof BingClusterLayer
*/
constructor(private _layer: Microsoft.Maps.ClusterLayer, private _maps: MapService) { }
///
/// Public methods, Layer interface implementation
///
/**
* Adds an event listener for the layer.
*
* @param eventType string. Type of event to add (click, mouseover, etc). You can use any event that the underlying native
* layer supports.
* @param fn function. Handler to call when the event occurs.
*
* @memberof BingClusterLayer
*/
public AddListener(eventType: string, fn: Function): void {
Microsoft.Maps.Events.addHandler(this._layer, eventType, (e) => {
fn(e);
});
}
/**
* Adds an entity to the layer. Use this method with caution as it will
* trigger a recaluation of the clusters (and associated markers if approprite) for
* each invocation. If you use this method to add many markers to the cluster, use
*
* @param entity Marker. Entity to add to the layer.
*
* @memberof BingClusterLayer
*/
public AddEntity(entity: Marker): void {
let isMarker: boolean = entity instanceof Marker;
isMarker = entity instanceof BingMarker || isMarker;
if (isMarker) {
if (entity.IsFirst) {
this.StopClustering();
}
}
if (entity.NativePrimitve && entity.Location) {
if (this._isClustering) {
const p: Array<Microsoft.Maps.Pushpin> = this._layer.getPushpins();
p.push(entity.NativePrimitve);
this._layer.setPushpins(p);
this._markers.push(entity);
}
else {
this._pendingMarkers.push(entity);
}
this._markerLookup.set(entity.NativePrimitve, entity);
}
if (isMarker) {
if (entity.IsLast) {
this.StartClustering();
}
}
}
/**
* Adds a number of markers to the layer.
*
* @param entities Array<Marker>. Entities to add to the layer.
*
* @memberof BingClusterLayer
*/
public AddEntities(entities: Array<Marker>): void {
if (entities != null && Array.isArray(entities) && entities.length !== 0 ) {
const e: Array<Microsoft.Maps.Pushpin> = entities.map(p => {
this._markerLookup.set(p.NativePrimitve, p);
return p.NativePrimitve;
});
if (this._isClustering) {
const p: Array<Microsoft.Maps.Pushpin> = this._layer.getPushpins();
p.push(...e);
this._layer.setPushpins(p);
this._markers.push(...entities);
}
else {
this._pendingMarkers.push(...entities);
}
}
}
/**
* Initializes spider behavior for the clusering layer (when a cluster maker is clicked, it explodes into a spider of the
* individual underlying pins.
*
* @param options ISpiderClusterOptions. Optional. Options governing the behavior of the spider.
*
* @memberof BingClusterLayer
*/
public InitializeSpiderClusterSupport(options?: ISpiderClusterOptions): void {
if (this._useSpiderCluster) { return; }
const m: Microsoft.Maps.Map = (<BingMapService>this._maps).MapInstance;
this._useSpiderCluster = true;
this._spiderLayer = new Microsoft.Maps.Layer();
this._currentZoom = m.getZoom();
this.SetSpiderOptions(options);
m.layers.insert(this._spiderLayer);
///
/// Add spider related events....
///
this._events.push(Microsoft.Maps.Events.addHandler(m, 'click', e => this.OnMapClick(e)));
this._events.push(Microsoft.Maps.Events.addHandler(m, 'viewchangestart', e => this.OnMapViewChangeStart(e)));
this._events.push(Microsoft.Maps.Events.addHandler(m, 'viewchangeend', e => this.OnMapViewChangeEnd(e)));
this._events.push(Microsoft.Maps.Events.addHandler(this._layer, 'click', e => this.OnLayerClick(e)));
this._events.push(Microsoft.Maps.Events.addHandler(this._spiderLayer, 'click', e => this.OnLayerClick(e)));
this._events.push(Microsoft.Maps.Events.addHandler(this._spiderLayer, 'mouseover', e => this.OnSpiderMouseOver(e)));
this._events.push(Microsoft.Maps.Events.addHandler(this._spiderLayer, 'mouseout', e => this.OnSpiderMouseOut(e)));
}
/**
* Deletes the clustering layer.
*
* @memberof BingClusterLayer
*/
public Delete(): void {
if (this._useSpiderCluster) {
this._spiderLayer.clear();
(<BingMapService>this._maps).MapPromise.then(m => {
m.layers.remove(this._spiderLayer);
this._spiderLayer = null;
});
this._events.forEach(e => Microsoft.Maps.Events.removeHandler(e));
this._events.splice(0);
this._useSpiderCluster = false;
}
this._markers.splice(0);
this._spiderMarkers.splice(0);
this._pendingMarkers.splice(0);
this._markerLookup.clear();
this._maps.DeleteLayer(this);
}
/**
* Returns the abstract marker used to wrap the Bing Pushpin.
*
* @returns Marker. The abstract marker object representing the pushpin.
*
* @memberof BingClusterLayer
*/
public GetMarkerFromBingMarker(pin: Microsoft.Maps.Pushpin): Marker {
const m: Marker = this._markerLookup.get(pin);
return m;
}
/**
* Returns the options governing the behavior of the layer.
*
* @returns IClusterOptions. The layer options.
*
* @memberof BingClusterLayer
*/
public GetOptions(): IClusterOptions {
const o: Microsoft.Maps.IClusterLayerOptions = this._layer.getOptions();
const options: IClusterOptions = {
id: 0,
gridSize: o.gridSize,
layerOffset: o.layerOffset,
clusteringEnabled: o.clusteringEnabled,
callback: o.callback,
clusteredPinCallback: o.clusteredPinCallback,
visible: o.visible,
zIndex: o.zIndex
};
return options;
}
/**
* Returns the visibility state of the layer.
*
* @returns Boolean. True is the layer is visible, false otherwise.
*
* @memberof BingClusterLayer
*/
public GetVisible(): boolean {
return this._layer.getOptions().visible;
}
/**
* Returns the abstract marker used to wrap the Bing Pushpin.
*
* @returns - The abstract marker object representing the pushpin.
*
* @memberof BingClusterLayer
*/
public GetSpiderMarkerFromBingMarker(pin: Microsoft.Maps.Pushpin): BingSpiderClusterMarker {
const m: BingSpiderClusterMarker = this._spiderMarkerLookup.get(pin);
return m;
}
/**
* Removes an entity from the cluster layer.
*
* @param entity Marker - Entity to be removed from the layer.
*
* @memberof BingClusterLayer
*/
public RemoveEntity(entity: Marker): void {
if (entity.NativePrimitve && entity.Location) {
const j: number = this._markers.indexOf(entity);
const k: number = this._pendingMarkers.indexOf(entity);
if (j > -1) { this._markers.splice(j, 1); }
if (k > -1) { this._pendingMarkers.splice(k, 1); }
if (this._isClustering) {
const p: Array<Microsoft.Maps.Pushpin> = this._layer.getPushpins();
const i: number = p.indexOf(entity.NativePrimitve);
if (i > -1) {
p.splice(i, 1);
this._layer.setPushpins(p);
}
}
this._markerLookup.delete(entity.NativePrimitve);
}
}
/**
* Sets the entities for the cluster layer.
*
* @param entities Array<Marker> containing
* the entities to add to the cluster. This replaces any existing entities.
*
* @memberof BingClusterLayer
*/
public SetEntities(entities: Array<Marker>): void {
const p: Array<Microsoft.Maps.Pushpin> = new Array<Microsoft.Maps.Pushpin>();
this._markers.splice(0);
this._markerLookup.clear();
entities.forEach((e: any) => {
if (e.NativePrimitve && e.Location) {
this._markers.push(e);
this._markerLookup.set(e.NativePrimitve, e);
p.push(<Microsoft.Maps.Pushpin>e.NativePrimitve);
}
});
this._layer.setPushpins(p);
}
/**
* Sets the options for the cluster layer.
*
* @param options IClusterOptions containing the options enumeration controlling the layer behavior. The supplied options
* are merged with the default/existing options.
*
* @memberof BingClusterLayer
*/
public SetOptions(options: IClusterOptions): void {
const o: Microsoft.Maps.IClusterLayerOptions = BingConversions.TranslateClusterOptions(options);
this._layer.setOptions(o);
if (options.spiderClusterOptions) { this.SetSpiderOptions(options.spiderClusterOptions); }
}
/**
* Toggles the cluster layer visibility.
*
* @param visible Boolean true to make the layer visible, false to hide the layer.
*
* @memberof BingClusterLayer
*/
public SetVisible(visible: boolean): void {
const o: Microsoft.Maps.IClusterLayerOptions = this._layer.getOptions();
o.visible = visible;
this._layer.setOptions(o);
}
/**
* 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.
*
* @memberof BingClusterLayer
*/
public StartClustering(): void {
if (this._isClustering) { return; }
const p: Array<Microsoft.Maps.Pushpin> = new Array<Microsoft.Maps.Pushpin>();
this._markers.forEach(e => {
if (e.NativePrimitve && e.Location) {
p.push(<Microsoft.Maps.Pushpin>e.NativePrimitve);
}
});
this._pendingMarkers.forEach(e => {
if (e.NativePrimitve && e.Location) {
p.push(<Microsoft.Maps.Pushpin>e.NativePrimitve);
}
});
this._layer.setPushpins(p);
this._markers = this._markers.concat(this._pendingMarkers.splice(0));
this._isClustering = true;
}
/**
* 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.
*
* @memberof BingClusterLayer
*/
public StopClustering() {
if (!this._isClustering) { return; }
this._isClustering = false;
}
///
/// Private methods
///
/**
* Creates a copy of a pushpins basic options.
*
* @param pin Pushpin to copy options from.
* @returns - A copy of a pushpins basic options.
*
* @memberof BingClusterLayer
*/
private GetBasicPushpinOptions(pin: Microsoft.Maps.Pushpin): Microsoft.Maps.IPushpinOptions {
return <Microsoft.Maps.IPushpinOptions>{
anchor: pin.getAnchor(),
color: pin.getColor(),
cursor: pin.getCursor(),
icon: pin.getIcon(),
roundClickableArea: pin.getRoundClickableArea(),
subTitle: pin.getSubTitle(),
text: pin.getText(),
textOffset: pin.getTextOffset(),
title: pin.getTitle()
};
}
/**
* Hides the spider cluster and resotres the original pin.
*
* @memberof BingClusterLayer
*/
private HideSpiderCluster(): void {
this._mapclicks = 0;
if (this._currentCluster) {
this._spiderLayer.clear();
this._spiderMarkers.splice(0);
this._spiderMarkerLookup.clear();
this._currentCluster = null;
this._mapclicks = -1;
if (this._spiderOptions.markerUnSelected) { this._spiderOptions.markerUnSelected(); }
}
}
/**
* Click event handler for when a shape in the cluster layer is clicked.
*
* @param e The mouse event argurment from the click event.
*
* @memberof BingClusterLayer
*/
private OnLayerClick(e: Microsoft.Maps.IMouseEventArgs): void {
if (e.primitive instanceof Microsoft.Maps.ClusterPushpin) {
const cp: Microsoft.Maps.ClusterPushpin = <Microsoft.Maps.ClusterPushpin>e.primitive;
const showNewCluster: boolean = cp !== this._currentCluster;
this.HideSpiderCluster();
if (showNewCluster) {
this.ShowSpiderCluster(<Microsoft.Maps.ClusterPushpin>e.primitive);
}
} else {
const pin: Microsoft.Maps.Pushpin = <Microsoft.Maps.Pushpin>e.primitive;
if (pin.metadata && pin.metadata.isClusterMarker) {
const m: BingSpiderClusterMarker = this.GetSpiderMarkerFromBingMarker(pin);
const p: BingMarker = m.ParentMarker;
const ppin: Microsoft.Maps.Pushpin = p.NativePrimitve;
if (this._spiderOptions.markerSelected) {
this._spiderOptions.markerSelected(p, new BingMarker(this._currentCluster, null, null));
}
if (Microsoft.Maps.Events.hasHandler(ppin, 'click')) { Microsoft.Maps.Events.invoke(ppin, 'click', e); }
this._mapclicks = 0;
} else {
if (this._spiderOptions.markerSelected) { this._spiderOptions.markerSelected(this.GetMarkerFromBingMarker(pin), null); }
if (Microsoft.Maps.Events.hasHandler(pin, 'click')) { Microsoft.Maps.Events.invoke(pin, 'click', e); }
}
}
}
/**
* Delegate handling the click event on the map (outside a spider cluster). Depending on the
* spider options, closes the cluster or increments the click counter.
*
* @param e - Mouse event
*
* @memberof BingClusterLayer
*/
private OnMapClick(e: Microsoft.Maps.IMouseEventArgs | Microsoft.Maps.IMapTypeChangeEventArgs): void {
if (this._mapclicks === -1) {
return;
} else if (++this._mapclicks >= this._spiderOptions.collapseClusterOnNthClick) {
this.HideSpiderCluster();
} else {
// do nothing as this._mapclicks has already been incremented above
}
}
/**
* Delegate handling the map view changed end event. Hides the spider cluster if the zoom level has changed.
*
* @param e - Mouse event.
*
* @memberof BingClusterLayer
*/
private OnMapViewChangeEnd(e: Microsoft.Maps.IMouseEventArgs | Microsoft.Maps.IMapTypeChangeEventArgs): void {
const z: number = (<Microsoft.Maps.Map>e.target).getZoom();
const hasZoomChanged: boolean = (z !== this._currentZoom);
this._currentZoom = z;
if (hasZoomChanged) {
this.HideSpiderCluster();
}
}
/**
* Delegate handling the map view change start event. Depending on the spider options, hides the
* the exploded spider or does nothing.
*
* @param e - Mouse event.
*
* @memberof BingClusterLayer
*/
private OnMapViewChangeStart(e: Microsoft.Maps.IMouseEventArgs | Microsoft.Maps.IMapTypeChangeEventArgs): void {
if (this._spiderOptions.collapseClusterOnMapChange) {
this.HideSpiderCluster();
}
}
/**
* Delegate invoked on mouse out on an exploded spider marker. Resets the hover style on the stick.
*
* @param e - Mouse event.
*/
private OnSpiderMouseOut(e: Microsoft.Maps.IMouseEventArgs): void {
const pin: Microsoft.Maps.Pushpin = <Microsoft.Maps.Pushpin>e.primitive;
if (pin instanceof Microsoft.Maps.Pushpin && pin.metadata && pin.metadata.isClusterMarker) {
const m: BingSpiderClusterMarker = this.GetSpiderMarkerFromBingMarker(pin);
m.Stick.setOptions(this._spiderOptions.stickStyle);
}
}
/**
* Invoked on mouse over on an exploded spider marker. Sets the hover style on the stick. Also invokes the click event
* on the underlying original marker dependent on the spider options.
*
* @param e - Mouse event.
*/
private OnSpiderMouseOver(e: Microsoft.Maps.IMouseEventArgs): void {
const pin: Microsoft.Maps.Pushpin = <Microsoft.Maps.Pushpin>e.primitive;
if (pin instanceof Microsoft.Maps.Pushpin && pin.metadata && pin.metadata.isClusterMarker) {
const m: BingSpiderClusterMarker = this.GetSpiderMarkerFromBingMarker(pin);
m.Stick.setOptions(this._spiderOptions.stickHoverStyle);
if (this._spiderOptions.invokeClickOnHover) {
const p: BingMarker = m.ParentMarker;
const ppin: Microsoft.Maps.Pushpin = p.NativePrimitve;
if (Microsoft.Maps.Events.hasHandler(ppin, 'click')) { Microsoft.Maps.Events.invoke(ppin, 'click', e); }
}
}
}
/**
* Sets the options for spider behavior.
*
* @param options ISpiderClusterOptions containing the options enumeration controlling the spider cluster behavior. The supplied options
* are merged with the default/existing options.
*
* @memberof BingClusterLayer
*/
private SetSpiderOptions(options: ISpiderClusterOptions): void {
if (options) {
if (typeof options.circleSpiralSwitchover === 'number') {
this._spiderOptions.circleSpiralSwitchover = options.circleSpiralSwitchover;
}
if (typeof options.collapseClusterOnMapChange === 'boolean') {
this._spiderOptions.collapseClusterOnMapChange = options.collapseClusterOnMapChange;
}
if (typeof options.collapseClusterOnNthClick === 'number') {
this._spiderOptions.collapseClusterOnNthClick = options.collapseClusterOnNthClick;
}
if (typeof options.invokeClickOnHover === 'boolean') {
this._spiderOptions.invokeClickOnHover = options.invokeClickOnHover;
}
if (typeof options.minSpiralAngleSeperation === 'number') {
this._spiderOptions.minSpiralAngleSeperation = options.minSpiralAngleSeperation;
}
if (typeof options.spiralDistanceFactor === 'number') {
this._spiderOptions.spiralDistanceFactor = options.spiralDistanceFactor;
}
if (typeof options.minCircleLength === 'number') {
this._spiderOptions.minCircleLength = options.minCircleLength;
}
if (options.stickHoverStyle) {
this._spiderOptions.stickHoverStyle = options.stickHoverStyle;
}
if (options.stickStyle) {
this._spiderOptions.stickStyle = options.stickStyle;
}
if (options.markerSelected) {
this._spiderOptions.markerSelected = options.markerSelected;
}
if (options.markerUnSelected) {
this._spiderOptions.markerUnSelected = options.markerUnSelected;
}
if (typeof options.visible === 'boolean') {
this._spiderOptions.visible = options.visible;
}
this.SetOptions(<IClusterOptions>options);
}
}
/**
* Expands a cluster into it's open spider layout.
*
* @param cluster The cluster to show in it's open spider layout..
*
* @memberof BingClusterLayer
*/
private ShowSpiderCluster(cluster: Microsoft.Maps.ClusterPushpin): void {
this.HideSpiderCluster();
this._currentCluster = cluster;
if (cluster && cluster.containedPushpins) {
// Create spider data.
const m: Microsoft.Maps.Map = (<BingMapService>this._maps).MapInstance;
const pins: Array<Microsoft.Maps.Pushpin> = cluster.containedPushpins;
const center: Microsoft.Maps.Location = cluster.getLocation();
const centerPoint: Microsoft.Maps.Point =
<Microsoft.Maps.Point>m.tryLocationToPixel(center, Microsoft.Maps.PixelReference.control);
let stick: Microsoft.Maps.Polyline;
let angle = 0;
const makeSpiral: boolean = pins.length > this._spiderOptions.circleSpiralSwitchover;
let legPixelLength: number;
let stepAngle: number;
let stepLength: number;
if (makeSpiral) {
legPixelLength = this._spiderOptions.minCircleLength / Math.PI;
stepLength = 2 * Math.PI * this._spiderOptions.spiralDistanceFactor;
}
else {
stepAngle = 2 * Math.PI / pins.length;
legPixelLength = (this._spiderOptions.spiralDistanceFactor / stepAngle / Math.PI / 2) * pins.length;
if (legPixelLength < this._spiderOptions.minCircleLength) { legPixelLength = this._spiderOptions.minCircleLength; }
}
for (let i = 0, len = pins.length; i < len; i++) {
// Calculate spider pin location.
if (!makeSpiral) {
angle = stepAngle * i;
}
else {
angle += this._spiderOptions.minSpiralAngleSeperation / legPixelLength + i * 0.0005;
legPixelLength += stepLength / angle;
}
const point: Microsoft.Maps.Point =
new Microsoft.Maps.Point(centerPoint.x + legPixelLength * Math.cos(angle),
centerPoint.y + legPixelLength * Math.sin(angle));
const loc: Microsoft.Maps.Location =
<Microsoft.Maps.Location>m.tryPixelToLocation(point, Microsoft.Maps.PixelReference.control);
// Create stick to pin.
stick = new Microsoft.Maps.Polyline([center, loc], this._spiderOptions.stickStyle);
this._spiderLayer.add(stick);
// Create pin in spiral that contains same metadata as parent pin.
const pin: Microsoft.Maps.Pushpin = new Microsoft.Maps.Pushpin(loc);
pin.metadata = pins[i].metadata || {};
pin.metadata.isClusterMarker = true;
pin.setOptions(this.GetBasicPushpinOptions(pins[i]));
this._spiderLayer.add(pin);
const spiderMarker: BingSpiderClusterMarker = new BingSpiderClusterMarker(pin, null, this._spiderLayer);
spiderMarker.Stick = stick;
spiderMarker.ParentMarker = <BingMarker>this.GetMarkerFromBingMarker(pins[i]);
this._spiderMarkers.push(spiderMarker);
this._spiderMarkerLookup.set(pin, spiderMarker);
}
this._mapclicks = 0;
}
}
}