src/components/map-polyline-layer.ts
import {
Directive, SimpleChange, Input, Output, OnDestroy, OnChanges,
EventEmitter, ContentChild, AfterContentInit, ViewContainerRef, NgZone,
SimpleChanges
} from '@angular/core';
import { Subscription } from 'rxjs';
import { IPoint } from '../interfaces/ipoint';
import { ISize } from '../interfaces/isize';
import { ILatLong } from '../interfaces/ilatlong';
import { IPolylineEvent } from '../interfaces/ipolyline-event';
import { IPolylineOptions } from '../interfaces/ipolyline-options';
import { ILayerOptions } from '../interfaces/ilayer-options';
import { ILabelOptions } from '../interfaces/ilabel-options';
import { LayerService } from '../services/layer.service';
import { MapService } from '../services/map.service';
import { Layer } from '../models/layer';
import { Polyline } from '../models/polyline';
import { MapLabel } from '../models/map-label';
import { CanvasOverlay } from '../models/canvas-overlay';
/**
* internal counter to use as ids for polylines.
*/
let layerId = 1000000;
/**
* MapPolylineLayerDirective performantly renders a large set of polyline on a {@link MapComponent}.
*
* ### Example
* ```typescript
* import {Component} from '@angular/core';
* import {MapComponent} from '...';
*
* @Component({
* selector: 'my-map-cmp',
* styles: [`
* .map-container {
* height: 300px;
* }
* `],
* template: `
* <x-map [Latitude]="lat" [Longitude]="lng" [Zoom]="zoom">
* <x-map-polyline-layer [PolygonOptions]="_polyline"></x-map-polyline-layer>
* </x-map>
* `
* })
* ```
*
* @export
*/
@Directive({
selector: 'x-map-polyline-layer'
})
export class MapPolylineLayerDirective implements OnDestroy, OnChanges, AfterContentInit {
///
/// Field declarations
///
private _id: number;
private _layerPromise: Promise<Layer>;
private _service: LayerService;
private _canvas: CanvasOverlay;
private _labels: Array<{loc: ILatLong, title: string}> = new Array<{loc: ILatLong, title: string}>();
private _tooltip: MapLabel;
private _tooltipSubscriptions: Array<Subscription> = new Array<Subscription>();
private _tooltipVisible: boolean = false;
private _defaultOptions: ILabelOptions = {
fontSize: 11,
fontFamily: 'sans-serif',
strokeWeight: 2,
strokeColor: '#000000',
fontColor: '#ffffff'
};
private _streaming: boolean = false;
private _polylines: Array<IPolylineOptions> = new Array<IPolylineOptions>();
private _polylinesLast: Array<IPolylineOptions> = new Array<IPolylineOptions>();
/**
* Set the maximum zoom at which the polyline labels are visible. Ignored if ShowLabel is false.
* @memberof MapPolylineLayerDirective
*/
@Input() public LabelMaxZoom: number = Number.MAX_SAFE_INTEGER;
/**
* Set the minimum zoom at which the polyline labels are visible. Ignored if ShowLabel is false.
* @memberof MapPolylineLayerDirective
*/
@Input() public LabelMinZoom: number = -1;
/**
* Sepcifies styleing options for on-map polyline labels.
*
* @memberof MapPolylineLayerDirective
*/
@Input() public LabelOptions: ILabelOptions;
/**
* Gets or sets An offset applied to the positioning of the layer.
*
* @memberof MapPolylineLayerDirective
*/
@Input() public LayerOffset: IPoint = null;
/**
* An array of polyline options representing the polylines in the layer.
*
* @memberof MapPolylineLayerDirective
*/
@Input()
public get PolylineOptions(): Array<IPolylineOptions> { return this._polylines; }
public set PolylineOptions(val: Array<IPolylineOptions>) {
if (this._streaming) {
this._polylinesLast.push(...val.slice(0));
this._polylines.push(...val);
}
else {
this._polylines = val.slice(0);
}
}
/**
* Whether to show the polylines titles as the labels on the polylines.
*
* @memberof MapPolylineLayerDirective
*/
@Input() public ShowLabels: boolean = false;
/**
* Whether to show the titles of the polylines as the tooltips on the polylines.
*
* @memberof MapPolylineLayerDirective
*/
@Input() public ShowTooltips: boolean = true;
/**
* Sets whether to treat changes in the PolylineOptions as streams of new markers. In this mode, changing the
* Array supplied in PolylineOptions will be incrementally drawn on the map as opposed to replace the polylines on the map.
*
* @memberof MapPolylineLayerDirective
*/
@Input()
public get TreatNewPolylineOptionsAsStream(): boolean { return this._streaming; }
public set TreatNewPolylineOptionsAsStream(val: boolean) { this._streaming = val; }
/**
* Sets the visibility of the marker layer
*
* @memberof MapPolylineLayerDirective
*/
@Input() public Visible: boolean;
/**
* Gets or sets the z-index of the layer. If not used, layers get stacked in the order created.
*
* @memberof MapPolylineLayerDirective
*/
@Input() public ZIndex: number = 0;
///
/// Delegates
///
/**
* This event emitter gets emitted when the user clicks a polyline in the layer.
*
* @memberof MapPolylineLayerDirective
*/
@Output() public PolylineClick: EventEmitter<IPolylineEvent> = new EventEmitter<IPolylineEvent>();
/**
* This event is fired when the DOM dblclick event is fired on a polyline in the layer.
*
* @memberof MapPolylineLayerDirective
*/
@Output() PolylineDblClick: EventEmitter<IPolylineEvent> = new EventEmitter<IPolylineEvent>();
/**
* This event is fired when the DOM mousemove event is fired on a polyline in the layer.
*
* @memberof MapPolylineLayerDirective
*/
@Output() PolylineMouseMove: EventEmitter<IPolylineEvent> = new EventEmitter<IPolylineEvent>();
/**
* This event is fired on mouseout on a polyline in the layer.
*
* @memberof MapPolylineLayerDirective
*/
@Output() PolylineMouseOut: EventEmitter<IPolylineEvent> = new EventEmitter<IPolylineEvent>();
/**
* This event is fired on mouseover on a polyline in a layer.
*
* @memberof MapPolylineLayerDirective
*/
@Output() PolylineMouseOver: EventEmitter<IPolylineEvent> = new EventEmitter<IPolylineEvent>();
///
/// Property declarations
///
/**
* Gets the id of the polyline layer.
*
* @readonly
* @memberof MapPolylineLayerDirective
*/
public get Id(): number { return this._id; }
///
/// Constructor
///
/**
* Creates an instance of MapPolylineLayerDirective.
* @param _layerService - Concreate implementation of a {@link LayerService}.
* @param _mapService - Concreate implementation of a {@link MapService}.
* @param _zone - Concreate implementation of a {@link NgZone} service.
* @memberof MapPolylineLayerDirective
*/
constructor(
private _layerService: LayerService,
private _mapService: MapService,
private _zone: NgZone) {
this._id = layerId++;
}
///
/// Public methods
///
/**
* Called after Component content initialization. Part of ng Component life cycle.
*
* @memberof MapPolylineLayerDirective
*/
public ngAfterContentInit() {
const layerOptions: ILayerOptions = {
id: this._id
};
this._zone.runOutsideAngular(() => {
const fakeLayerDirective: any = {
Id : this._id,
Visible: this.Visible,
LayerOffset: this.LayerOffset,
ZIndex: this.ZIndex
};
this._layerService.AddLayer(fakeLayerDirective);
this._layerPromise = this._layerService.GetNativeLayer(fakeLayerDirective);
Promise.all([
this._layerPromise,
this._mapService.CreateCanvasOverlay(el => this.DrawLabels(el))
]).then(values => {
values[0].SetVisible(this.Visible);
this._canvas = values[1];
this._canvas._canvasReady.then(b => {
this._tooltip = this._canvas.GetToolTipOverlay();
this.ManageTooltip(this.ShowTooltips);
});
if (this.PolylineOptions) {
this._zone.runOutsideAngular(() => this.UpdatePolylines());
}
});
this._service = this._layerService;
});
}
/**
* Called on component destruction. Frees the resources used by the component. Part of the ng Component life cycle.
*
* @memberof MapPolylineLayerDirective
*/
public ngOnDestroy() {
this._tooltipSubscriptions.forEach(s => s.unsubscribe());
this._layerPromise.then(l => {
l.Delete();
});
if (this._canvas) { this._canvas.Delete(); }
}
/**
* 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 MapPolylineLayerDirective
*/
public ngOnChanges(changes: { [key: string]: SimpleChange }) {
if (changes['PolylineOptions']) {
this._zone.runOutsideAngular(() => {
this.UpdatePolylines();
});
}
if (changes['Visible'] && !changes['Visible'].firstChange) {
this._layerPromise.then(l => l.SetVisible(this.Visible));
}
if ((changes['ZIndex'] && !changes['ZIndex'].firstChange) ||
(changes['LayerOffset'] && !changes['LayerOffset'].firstChange)
) {
throw (new Error('You cannot change ZIndex or LayerOffset after the layer has been created.'));
}
if ((changes['ShowLabels'] && !changes['ShowLabels'].firstChange) ||
(changes['LabelMinZoom'] && !changes['LabelMinZoom'].firstChange) ||
(changes['LabelMaxZoom'] && !changes['LabelMaxZoom'].firstChange)
) {
if (this._canvas) {
this._canvas.Redraw(true);
}
}
if (changes['ShowTooltips'] && this._tooltip) {
this.ManageTooltip(changes['ShowTooltips'].currentValue);
}
}
/**
* Obtains a string representation of the Layer Id.
* @returns - string representation of the layer id.
* @memberof MapPolylineLayerDirective
*/
public toString(): string { return 'MapPolylineLayer-' + this._id.toString(); }
///
/// Private methods
///
/**
* Adds various event listeners for the polylines.
*
* @param p - the polyline for which to add the event.
*
* @memberof MapPolylineLayerDirective
*/
private AddEventListeners(p: Polyline): void {
const handlers = [
{ name: 'click', handler: (ev: MouseEvent) => this.PolylineClick.emit({Polyline: p, Click: ev}) },
{ name: 'dblclick', handler: (ev: MouseEvent) => this.PolylineDblClick.emit({Polyline: p, Click: ev}) },
{ name: 'mousemove', handler: (ev: MouseEvent) => this.PolylineMouseMove.emit({Polyline: p, Click: ev}) },
{ name: 'mouseout', handler: (ev: MouseEvent) => this.PolylineMouseOut.emit({Polyline: p, Click: ev}) },
{ name: 'mouseover', handler: (ev: MouseEvent) => this.PolylineMouseOver.emit({Polyline: p, Click: ev}) }
];
handlers.forEach((obj) => p.AddListener(obj.name, obj.handler));
}
/**
* Draws the polyline labels. Called by the Canvas overlay.
*
* @param el - The canvas on which to draw the labels.
* @memberof MapPolylineLayerDirective
*/
private DrawLabels(el: HTMLCanvasElement): void {
if (this.ShowLabels) {
this._mapService.GetZoom().then(z => {
if (this.LabelMinZoom <= z && this.LabelMaxZoom >= z) {
const ctx: CanvasRenderingContext2D = el.getContext('2d');
const labels = this._labels.map(x => x.title);
this._mapService.LocationsToPoints(this._labels.map(x => x.loc)).then(locs => {
const size: ISize = this._mapService.MapSize;
for (let i = 0, len = locs.length; i < len; i++) {
// Don't draw the point if it is not in view. This greatly improves performance when zoomed in.
if (locs[i].x >= 0 && locs[i].y >= 0 && locs[i].x <= size.width && locs[i].y <= size.height) {
this.DrawText(ctx, locs[i], labels[i]);
}
}
});
}
});
}
}
/**
* Draws the label text at the appropriate place on the canvas.
* @param ctx - Canvas drawing context.
* @param loc - Pixel location on the canvas where to center the text.
* @param text - Text to draw.
*/
private DrawText(ctx: CanvasRenderingContext2D, loc: IPoint, text: string) {
let lo: ILabelOptions = this.LabelOptions;
if (lo == null && this._tooltip) { lo = this._tooltip.DefaultLabelStyle; }
if (lo == null) { lo = this._defaultOptions; }
ctx.strokeStyle = lo.strokeColor;
ctx.font = `${lo.fontSize}px ${lo.fontFamily}`;
ctx.textAlign = 'center';
const strokeWeight: number = lo.strokeWeight;
if (text && strokeWeight && strokeWeight > 0) {
ctx.lineWidth = strokeWeight;
ctx.strokeText(text, loc.x, loc.y);
}
ctx.fillStyle = lo.fontColor;
ctx.fillText(text, loc.x, loc.y);
}
/**
* Manages the tooltip and the attachment of the associated events.
*
* @param show - True to enable the tooltip, false to disable.
* @memberof MapPolygonLayerDirective
*/
private ManageTooltip(show: boolean): void {
if (show && this._canvas) {
// add tooltip subscriptions
this._tooltip.Set('hidden', true);
this._tooltipVisible = false;
this._tooltipSubscriptions.push(this.PolylineMouseMove.asObservable().subscribe(e => {
if (this._tooltipVisible) {
const loc: ILatLong = this._canvas.GetCoordinatesFromClick(e.Click);
this._tooltip.Set('position', loc);
}
}));
this._tooltipSubscriptions.push(this.PolylineMouseOver.asObservable().subscribe(e => {
if (e.Polyline.Title && e.Polyline.Title.length > 0) {
const loc: ILatLong = this._canvas.GetCoordinatesFromClick(e.Click);
this._tooltip.Set('text', e.Polyline.Title);
this._tooltip.Set('position', loc);
if (!this._tooltipVisible) {
this._tooltip.Set('hidden', false);
this._tooltipVisible = true;
}
}
}));
this._tooltipSubscriptions.push(this.PolylineMouseOut.asObservable().subscribe(e => {
if (this._tooltipVisible) {
this._tooltip.Set('hidden', true);
this._tooltipVisible = false;
}
}));
}
else {
// remove tooltip subscriptions
this._tooltipSubscriptions.forEach(s => s.unsubscribe());
this._tooltipSubscriptions.splice(0);
this._tooltip.Set('hidden', true);
this._tooltipVisible = false;
}
}
/**
* Sets or updates the polyliness based on the polyline options. This will place the polylines on the map
* and register the associated events.
*
* @memberof MapPolylineLayerDirective
* @method
*/
private UpdatePolylines(): void {
if (this._layerPromise == null) {
return;
}
this._layerPromise.then(l => {
const polylines: Array<IPolylineOptions> = this._streaming ? this._polylinesLast.splice(0) : this._polylines;
if (!this._streaming) { this._labels.splice(0); }
// generate the promise for the polylines
const lp: Promise<Array<Polyline|Array<Polyline>>> = this._service.CreatePolylines(l.GetOptions().id, polylines);
// set polylines once promises are fullfilled.
lp.then(p => {
const y: Array<Polyline> = new Array<Polyline>();
p.forEach(poly => {
if (Array.isArray(poly)) {
let title: string = '';
const centroids: Array<ILatLong> = new Array<ILatLong>();
poly.forEach(x => {
y.push(x);
this.AddEventListeners(x);
centroids.push(x.Centroid);
if (x.Title != null && x.Title.length > 0 && title.length === 0) { title = x.Title; }
});
this._labels.push({loc: Polyline.GetPolylineCentroid(centroids), title: title});
}
else {
y.push(poly);
if (poly.Title != null && poly.Title.length > 0) { this._labels.push({loc: poly.Centroid, title: poly.Title}); }
this.AddEventListeners(poly);
}
});
this._streaming ? l.AddEntities(y) : l.SetEntities(y);
if (this._canvas) { this._canvas.Redraw(!this._streaming); }
});
});
}
}