src/components/cluster-layer.ts
import { IClusterIconInfo } from '../interfaces/icluster-icon-info';
import { Directive, EventEmitter, OnInit, OnDestroy, OnChanges, SimpleChange,
ContentChildren, Input, ViewContainerRef } from '@angular/core';
import { Marker } from '../models/marker';
import { Layer } from '../models/layer';
import { ClusterPlacementMode } from '../models/cluster-placement-mode';
import { ClusterClickAction } from '../models/cluster-click-action';
import { IPoint } from '../interfaces/ipoint';
import { IClusterOptions } from '../interfaces/icluster-options';
import { IMarkerIconInfo} from '../interfaces/imarker-icon-info';
import { ClusterService } from '../services/cluster.service';
import { ISpiderClusterOptions } from '../interfaces/ispider-cluster-options';
import { MapMarkerDirective } from './map-marker';
import { MapLayerDirective } from './map-layer';
/**
*
* Creates a cluster layer on a {@link MapComponent}.
*
* ### Example
* ```typescript
* import {Component} from '@angular/core';
* import {MapComponent, MapMarkerDirective} from '...';
*
* @Component({
* selector: 'my-map-cmp',
* styles: [`
* .map-container {
* height: 300px;
* }
* `],
* template: `
* <x-map [Latitude]='lat' [Longitude]='lng' [Zoom]='zoom'>
* <x-cluster-layer [Visible]='visible'>
* <x-map-marker [Latitude]='lat' [Longitude]='lng' [Label]=''M''></x-map-marker>
* </x-cluster-layer>
* </x-map>
* `
* })
* ```
*
* @export
*/
@Directive({
selector: 'x-cluster-layer'
})
export class ClusterLayerDirective extends MapLayerDirective implements OnInit, OnDestroy, OnChanges {
///
/// Field declarations
///
private _clusteringEnabled = true;
private _clusterPlacementMode: ClusterPlacementMode = ClusterPlacementMode.MeanValue;
private _clusterClickAction: ClusterClickAction = ClusterClickAction.ZoomIntoCluster;
private _spiderClusterOptions: ISpiderClusterOptions;
private _zIndex: number;
private _gridSize: number;
private _layerOffset: IPoint;
private _iconInfo: IMarkerIconInfo;
private _minimumClusterSize: number;
private _styles: Array<IClusterIconInfo>;
private _useDynamicSizeMarker = false;
private _dynamicMarkerBaseSize = 18;
private _dynamicMarkerRanges: Map<number, string> = new Map<number, string>([
[10, 'rgba(20, 180, 20, 0.5)'],
[100, 'rgba(255, 210, 40, 0.5)'],
[Number.MAX_SAFE_INTEGER , 'rgba(255, 40, 40, 0.5)']
]);
private _zoomOnClick = true;
private _iconCreationCallback: (m: Array<Marker>, i: IMarkerIconInfo) => string;
///
/// Property defintions
///
/**
* Gets or sets the the Cluster Click Action {@link ClusterClickAction}.
*
* @memberof ClusterLayerDirective
*/
@Input()
public get ClusterClickAction(): ClusterClickAction { return this._clusterClickAction; }
public set ClusterClickAction(val: ClusterClickAction) { this._clusterClickAction = val; }
/**
* Gets or sets whether the clustering layer enables clustering. When set to false, the layer
* behaves like a generic layer. This is handy if you want to prevent clustering at certain zoom levels.
*
* @memberof ClusterLayerDirective
*/
@Input()
public get ClusteringEnabled(): boolean { return this._clusteringEnabled; }
public set ClusteringEnabled(val: boolean) { this._clusteringEnabled = val; }
/**
* Gets or sets the cluster placement mode. {@link ClusterPlacementMode}
*
* @memberof ClusterLayerDirective
*/
@Input()
public get ClusterPlacementMode(): ClusterPlacementMode { return this._clusterPlacementMode; }
public set ClusterPlacementMode(val: ClusterPlacementMode) { this._clusterPlacementMode = val; }
/**
* Gets or sets the callback invoked to create a custom cluster marker. Note that when {@link UseDynamicSizeMarkers} is enabled,
* you cannot set a custom marker callback.
*
* @memberof ClusterLayerDirective
*/
@Input()
public get CustomMarkerCallback(): (m: Array<Marker>, i: IMarkerIconInfo) => string { return this._iconCreationCallback; }
public set CustomMarkerCallback(val: (m: Array<Marker>, i: IMarkerIconInfo) => string) {
if (this._useDynamicSizeMarker) {
throw(
new Error(`You cannot set a custom marker callback when UseDynamicSizeMarkers is set to true.
Set UseDynamicSizeMakers to false.`)
);
}
this._iconCreationCallback = val;
}
/**
* Gets or sets the base size of dynamic markers in pixels. The actualy size of the dynamic marker is based on this.
* See {@link UseDynamicSizeMarkers}.
*
* @memberof ClusterLayerDirective
*/
@Input()
public get DynamicMarkerBaseSize(): number { return this._dynamicMarkerBaseSize; }
public set DynamicMarkerBaseSize(val: number) { this._dynamicMarkerBaseSize = val; }
/**
* Gets or sets the ranges to use to calculate breakpoints and colors for dynamic markers.
* The map contains key/value pairs, with the keys being
* the breakpoint sizes and the values the colors to be used for the dynamic marker in that range. See {@link UseDynamicSizeMarkers}.
*
* @memberof ClusterLayerDirective
*/
@Input()
public get DynamicMarkerRanges(): Map<number, string> { return this._dynamicMarkerRanges; }
public set DynamicMarkerRanges(val: Map<number, string>) { this._dynamicMarkerRanges = val; }
/**
* Gets or sets the grid size to be used for clustering.
*
* @memberof ClusterLayerDirective
*/
@Input()
public get GridSize(): number { return this._gridSize; }
public set GridSize(val: number) { this._gridSize = val; }
/**
* Gets or sets the IconInfo to be used to create a custom cluster marker. Supports font-based, SVG, graphics and more.
* See {@link IMarkerIconInfo}.
*
* @memberof ClusterLayerDirective
*/
@Input()
public get IconInfo(): IMarkerIconInfo { return this._iconInfo; }
public set IconInfo(val: IMarkerIconInfo) { this._iconInfo = val; }
/**
* Gets or sets An offset applied to the positioning of the layer.
*
* @memberof ClusterLayerDirective
*/
@Input()
public get LayerOffset(): IPoint { return this._layerOffset; }
public set LayerOffset(val: IPoint) { this._layerOffset = val; }
/**
* Gets or sets the minimum pins required to form a cluster
*
* @readonly
* @memberof ClusterLayerDirective
*/
@Input()
public get MinimumClusterSize(): number { return this._minimumClusterSize; }
public set MinimumClusterSize(val: number) { this._minimumClusterSize = val; }
/**
* Gets or sets the options for spider clustering behavior. See {@link ISpiderClusterOptions}
*
* @memberof ClusterLayerDirective
*/
@Input()
public get SpiderClusterOptions(): ISpiderClusterOptions { return this._spiderClusterOptions; }
public set SpiderClusterOptions(val: ISpiderClusterOptions) { this._spiderClusterOptions = val; }
/**
* Gets or sets the cluster styles
*
* @readonly
* @memberof ClusterLayerDirective
*/
@Input()
public get Styles(): Array<IClusterIconInfo> { return this._styles; }
public set Styles(val: Array<IClusterIconInfo>) { this._styles = val; }
/**
* Gets or sets whether to use dynamic markers. Dynamic markers change in size and color depending on the number of
* pins in the cluster. If set to true, this will take precendence over any custom marker creation.
*
* @memberof ClusterLayerDirective
*/
@Input()
public get UseDynamicSizeMarkers(): boolean { return this._useDynamicSizeMarker; }
public set UseDynamicSizeMarkers(val: boolean) {
this._useDynamicSizeMarker = val;
if (val) {
this._iconCreationCallback = (m: Array<Marker>, info: IMarkerIconInfo) => {
return ClusterLayerDirective.CreateDynamicSizeMarker(
m.length, info, this._dynamicMarkerBaseSize, this._dynamicMarkerRanges);
};
}
}
/**
* Gets or sets the z-index of the layer. If not used, layers get stacked in the order created.
*
* @memberof ClusterLayerDirective
*/
@Input()
public get ZIndex(): number { return this._zIndex; }
public set ZIndex(val: number) { this._zIndex = val; }
/**
* Gets or sets whether the cluster should zoom in on click
*
* @readonly
* @memberof ClusterLayerDirective
*/
@Input()
public get ZoomOnClick(): boolean { return this._zoomOnClick; }
public set ZoomOnClick(val: boolean) { this._zoomOnClick = val; }
/**
* Creates the dynamic size marker to be used for cluster markers if UseDynamicSizeMarkers is set to true.
*
* @param size - The number of markers in the cluster.
* @param info - The icon info to be used. This will be hydrated with
* the actualy dimensions of the created markers and is used by the underlying model/services
* to correctly offset the marker for correct positioning.
* @param baseMarkerSize - The base size for dynmic markers.
* @param ranges - The ranges to use to calculate breakpoints and colors for dynamic markers.
* The map contains key/value pairs, with the keys being
* the breakpoint sizes and the values the colors to be used for the dynamic marker in that range.
* @returns - An string containing the SVG for the marker.
*
* @memberof ClusterLayerDirective
*/
public static CreateDynamicSizeMarker(size: number, info: IMarkerIconInfo,
baseMarkerSize: number, ranges: Map<number, string>): string {
const mr: number = baseMarkerSize;
const outline: number = mr * 0.35;
const total: number = size;
const r: number = Math.log(total) / Math.log(10) * 5 + mr;
const d: number = r * 2;
let fillColor: string;
ranges.forEach((v, k) => {
if (total <= k && !fillColor) { fillColor = v; }
});
if (!fillColor) { fillColor = 'rgba(20, 180, 20, 0.5)'; }
// Create an SVG string of two circles, one on top of the other, with the specified radius and color.
const svg: Array<any> = [`<svg xmlns='http://www.w3.org/2000/svg' width='${d}' height='${d}'>`,
`<circle cx='${r}' cy='${r}' r='${r}' fill='${fillColor}'/>`,
`<circle cx='${r}' cy='${r}' r='${r - outline}' fill='${fillColor}'/>`,
`</svg>`];
info.size = { width: d, height: d };
info.markerOffsetRatio = { x: 0.5, y: 0.5 };
info.textOffset = { x: 0, y: r - 8 };
return svg.join('');
}
///
/// Constructor
///
/**
* Creates an instance of ClusterLayerDirective.
*
* @param _layerService - Concreted implementation of a cluster layer service for the underlying maps
* implementations. Generally provided via injections.
* @param _containerRef - A reference to the view container of the layer. Generally provided via injection.
*
* @memberof ClusterLayerDirective
*/
constructor(_layerService: ClusterService, _containerRef: ViewContainerRef) {
super(_layerService, _containerRef);
}
///
/// Public methods
///
/**
* Reacts to changes in data-bound properties of the component and actuates property changes in the underling layer model.
*
* @param changes - collection of changes.
*
* @memberof ClusterLayerDirective
*/
public ngOnChanges(changes: { [propName: string]: SimpleChange }): void {
if (!this._addedToManager) { return; }
if (changes['ClusterClickAction']) {
throw (
new Error('You cannot change the ClusterClickAction after the layer has been added to the layerservice.')
);
}
const options: IClusterOptions = { id: this._id };
if (changes['ClusteringEnabled']) { options.clusteringEnabled = this._clusteringEnabled; }
if (changes['GridSize']) { options.gridSize = this._gridSize; }
if (changes['LayerOffset']) { options.layerOffset = this._layerOffset; }
if (changes['SpiderClusterOptions']) { options.spiderClusterOptions = this._spiderClusterOptions; }
if (changes['ZIndex']) { options.zIndex = this._zIndex; }
if (changes['Visible']) { options.visible = this._visible; }
this._layerService.GetNativeLayer(this).then((l: Layer) => {
l.SetOptions(options);
});
}
}