infusion-code/angular-maps

View on GitHub
src/components/map-polygon.ts

Summary

Maintainability
D
2 days
Test Coverage
import {
    Directive, Input, Output, OnDestroy, OnChanges, ViewContainerRef,
    EventEmitter, ContentChild, AfterContentInit, SimpleChanges
} from '@angular/core';
import { Subscription } from 'rxjs';
import { IPolygonOptions } from '../interfaces/ipolygon-options';
import { IPoint } from '../interfaces/ipoint';
import { IPolygonEvent } from '../interfaces/ipolygon-event';
import { ILatLong } from '../interfaces/ilatlong';
import { PolygonService } from '../services/polygon.service';
import { InfoBoxComponent } from './infobox';

let polygonId = 0;

/**
 *
 * MapPolygonDirective renders a polygon inside a {@link MapComponent}.
 *
 * ### Example
 * ```typescript
 * import {Component} from '@angular/core';
 * import {MapComponent, MapPolygonDirective} from '...';
 *
 * @Component({
 *  selector: 'my-map,
 *  styles: [`
 *   .map-container { height: 300px; }
 * `],
 * template: `
 *   <x-map [Latitude]="lat" [Longitude]="lng" [Zoom]="zoom">
 *      <x-map-polygon [Paths]="path"></x-map-polygon>
 *   </x-map>
 * `
 * })
 * ```
 *
 *
 * @export
 */
@Directive({
    selector: 'x-map-polygon'
})
export class MapPolygonDirective implements OnDestroy, OnChanges, AfterContentInit {

    ///
    /// Field declarations
    ///
    private _inCustomLayer = false;
    private _id: number;
    private _layerId: number;
    private _addedToService = false;
    private _events: Subscription[] = [];

    ///
    /// Any InfoBox that is a direct children of the polygon
    ///
    @ContentChild(InfoBoxComponent) protected _infoBox: InfoBoxComponent;


    /**
     * Gets or sets whether this Polygon handles mouse events.
     *
     * @memberof MapPolygonDirective
     */
    @Input() public Clickable = true;

    /**
     * If set to true, the user can drag this shape over the map.
     *
     * @memberof MapPolygonDirective
     */
    @Input() public Draggable = false;

    /**
     * If set to true, the user can edit this shape by dragging the control
     * points shown at the vertices and on each segment.
     *
     * @memberof MapPolygonDirective
     */
    @Input() public Editable = false;

    /**
     * The fill color of the polygon.
     *
     * @memberof MapPolygonDirective
     */
    @Input() public FillColor: string;

    /**
     * The fill opacity between 0.0 and 1.0
     *
     * @memberof MapPolygonDirective
     */
    @Input() public FillOpacity: number;

    /**
     * When true, edges of the polygon are interpreted as geodesic and will
     * follow the curvature of the Earth. When false, edges of the polygon are
     * rendered as straight lines in screen space. Note that the shape of a
     * geodesic polygon may appear to change when dragged, as the dimensions
     * are maintained relative to the surface of the earth. Defaults to false.
     *
     * @memberof MapPolygonDirective
     */
    @Input() public Geodesic = false;

    /**
     * Set the maximum zoom at which the polygon lable is visible. Ignored if ShowLabel is false.
     * @memberof MapPolygonDirective
     */
    @Input() public LabelMaxZoom: number;

    /**
     * Set the minimum zoom at which the polygon lable is visible. Ignored if ShowLabel is false.
     * @memberof MapPolygonDirective
     */
    @Input() public LabelMinZoom: number;

    /**
     * Arbitary metadata to assign to the Polygon. This is useful for events
     *
     * @memberof MapPolygonDirective
     */
    @Input() public Metadata: Map<string, any> = new Map<string, any>();

    /**
     * The ordered sequence of coordinates that designates a closed loop.
     * Unlike polylines, a polygon may consist of one or more paths.
     * As a result, the paths property may specify one or more arrays of
     * LatLng coordinates. Paths are closed automatically; do not repeat the
     * first vertex of the path as the last vertex. Simple polygons may be
     * defined using a single array of LatLngs. More complex polygons may
     * specify an array of arrays (for inner loops ). Any simple arrays are converted into Arrays.
     * Inserting or removing LatLngs from the Array will automatically update
     * the polygon on the map.
     *
     * @memberof MapPolygonDirective
     */
    @Input() public Paths: Array<ILatLong> | Array<Array<ILatLong>> = [];

    /**
     * Whether to show the title as the label on the polygon.
     *
     * @memberof MapPolygonDirective
     */
    @Input() public ShowLabel: boolean;

    /**
     * Whether to show the title of the polygon as the tooltip on the polygon.
     *
     * @memberof MapPolygonDirective
     */
    @Input() public ShowTooltip: boolean = true;

    /**
     * The stroke color.
     *
     * @memberof MapPolygonDirective
     */
    @Input() public StrokeColor: string;

    /**
     * The stroke opacity between 0.0 and 1.0
     *
     * @memberof MapPolygonDirective
     */
    @Input() public StrokeOpacity: number;

    /**
     * The stroke width in pixels.
     *
     * @memberof MapPolygonDirective
     */
    @Input() public StrokeWeight: number;

    /**
     * The title of the polygon.
     *
     * @memberof MapPolygonDirective
     */
    @Input() public Title: string;

    /**
     * Whether this polygon is visible on the map. Defaults to true.
     *
     * @memberof MapPolygonDirective
     */
    @Input() public Visible: boolean;

    /**
     * The zIndex compared to other polys.
     *
     * @memberof MapPolygonDirective
     */
    @Input() public zIndex: number;

    ///
    /// Delegate definitions
    ///

    /**
     * This event is fired when the DOM click event is fired on the Polygon.
     *
     * @memberof MapPolygonDirective
     */
    @Output() Click: EventEmitter<IPolygonEvent> = new EventEmitter<IPolygonEvent>();

    /**
     * This event is fired when the DOM dblclick event is fired on the Polygon.
     *
     * @memberof MapPolygonDirective
     */
    @Output() DblClick: EventEmitter<IPolygonEvent> = new EventEmitter<IPolygonEvent>();

    /**
     * This event is repeatedly fired while the user drags the polygon.
     *
     * @memberof MapPolygonDirective
     */
    @Output() Drag: EventEmitter<IPolygonEvent> = new EventEmitter<IPolygonEvent>();

    /**
     * This event is fired when the user stops dragging the polygon.
     *
     * @memberof MapPolygonDirective
     */
    @Output() DragEnd: EventEmitter<IPolygonEvent> = new EventEmitter<IPolygonEvent>();

    /**
     * This event is fired when the user starts dragging the polygon.
     *
     * @memberof MapPolygonDirective
     */
    @Output() DragStart: EventEmitter<IPolygonEvent> = new EventEmitter<IPolygonEvent>();

    /**
     * This event is fired when the DOM mousedown event is fired on the Polygon.
     *
     * @memberof MapPolygonDirective
     */
    @Output() MouseDown: EventEmitter<IPolygonEvent> = new EventEmitter<IPolygonEvent>();

    /**
     * This event is fired when the DOM mousemove event is fired on the Polygon.
     *
     * @memberof MapPolygonDirective
     */
    @Output() MouseMove: EventEmitter<IPolygonEvent> = new EventEmitter<IPolygonEvent>();

    /**
     * This event is fired on Polygon mouseout.
     *
     * @memberof MapPolygonDirective
     */
    @Output() MouseOut: EventEmitter<IPolygonEvent> = new EventEmitter<IPolygonEvent>();

    /**
     * This event is fired on Polygon mouseover.
     *
     * @memberof MapPolygonDirective
     */
    @Output() MouseOver: EventEmitter<IPolygonEvent> = new EventEmitter<IPolygonEvent>();

    /**
     * This event is fired whe the DOM mouseup event is fired on the Polygon
     *
     * @memberof MapPolygonDirective
     */
    @Output() MouseUp: EventEmitter<IPolygonEvent> = new EventEmitter<IPolygonEvent>();


    /**
     * This event is fired when the Polygon is right-clicked on.
     *
     * @memberof MapPolygonDirective
     */
    @Output() RightClick: EventEmitter<IPolygonEvent> = new EventEmitter<IPolygonEvent>();

    /**
     * This event is fired when editing has completed.
     *
     * @memberof MapPolygonDirective
     */
    @Output() PathChanged: EventEmitter<IPolygonEvent> = new EventEmitter<IPolygonEvent>();

    ///
    /// Property declarations
    ///

    /**
     * Gets whether the polygon has been registered with the service.
     * @readonly
     * @memberof MapPolygonDirective
     */
    public get AddedToService(): boolean { return this._addedToService; }

    /**
     * Get the id of the polygon.
     *
     * @readonly
     * @memberof MapPolygonDirective
     */
    public get Id(): number { return this._id; }

    /**
     * Gets the id of the polygon as a string.
     *
     * @readonly
     * @memberof MapPolygonDirective
     */
    public get IdAsString(): string { return this._id.toString(); }

    /**
     * Gets whether the polygon is in a custom layer. See {@link MapLayer}.
     *
     * @readonly
     * @memberof MapPolygonDirective
     */
    public get InCustomLayer(): boolean { return this._inCustomLayer; }

    /**
     * gets the id of the Layer the polygon belongs to.
     *
     * @readonly
     * @memberof MapPolygonDirective
     */
    public get LayerId(): number { return this._layerId; }

    ///
    /// Constructor
    ///

    /**
     * Creates an instance of MapPolygonDirective.
     * @param _polygonManager
     *
     * @memberof MapPolygonDirective
     */
    constructor(private _polygonService: PolygonService, private _containerRef: ViewContainerRef) {
        this._id = polygonId++;
    }

    ///
    /// Public methods
    ///

    /**
     * Called after the content intialization of the directive is complete. Part of the ng Component life cycle.
     *
     * @memberof MapPolygonDirective
     */
    ngAfterContentInit(): void {
        if (this._containerRef.element.nativeElement.parentElement) {
            const parentName: string = this._containerRef.element.nativeElement.parentElement.tagName;
            if (parentName.toLowerCase() === 'x-map-layer') {
                this._inCustomLayer = true;
                this._layerId = Number(this._containerRef.element.nativeElement.parentElement.attributes['layerId']);
            }
        }
        if (!this._addedToService) {
            this._polygonService.AddPolygon(this);
            this._addedToService = true;
            this.AddEventListeners();
        }
        return;
    }

    /**
     * Called when changes to the databoud properties occur. Part of the ng Component life cycle.
     *
     * @param changes - Changes that have occured.
     *
     * @memberof MapPolygonDirective
     */
    ngOnChanges(changes: SimpleChanges): any {
        if (!this._addedToService) { return; }

        const o: IPolygonOptions = this.GeneratePolygonChangeSet(changes);
        if (o != null) { this._polygonService.SetOptions(this, o); }
        if (changes['Paths'] && !changes['Paths'].isFirstChange()) {
            this._polygonService.UpdatePolygon(this);
        }

    }

    /**
     * Called when the poygon is being destroyed. Part of the ng Component life cycle. Release resources.
     *
     *
     * @memberof MapPolygonDirective
     */
    ngOnDestroy() {
        this._polygonService.DeletePolygon(this);
        this._events.forEach((s) => s.unsubscribe());
        ///
        /// remove event subscriptions
        ///
    }

    ///
    /// Private methods
    ///

    /**
     * Wires up the event receivers.
     *
     * @memberof MapPolygonDirective
     */
    private AddEventListeners() {
        const _getEventArg: (e: MouseEvent) => IPolygonEvent = e => {
            return {
                Polygon: this,
                Click: e
            };
        };
        this._events.push(this._polygonService.CreateEventObservable('click', this).subscribe((ev: MouseEvent) => {
            const t: MapPolygonDirective = this;
            if (this._infoBox != null) {
                this._infoBox.Open(this._polygonService.GetCoordinatesFromClick(ev));
            }
            this.Click.emit(_getEventArg(ev));
        }));
        const handlers = [
            { name: 'dblclick', handler: (ev: MouseEvent) => this.DblClick.emit(_getEventArg(ev)) },
            { name: 'drag', handler: (ev: MouseEvent) => this.Drag.emit(_getEventArg(ev)) },
            { name: 'dragend', handler: (ev: MouseEvent) => this.DragEnd.emit(_getEventArg(ev)) },
            { name: 'dragstart', handler: (ev: MouseEvent) => this.DragStart.emit(_getEventArg(ev)) },
            { name: 'mousedown', handler: (ev: MouseEvent) => this.MouseDown.emit(_getEventArg(ev)) },
            { name: 'mousemove', handler: (ev: MouseEvent) => this.MouseMove.emit(_getEventArg(ev)) },
            { name: 'mouseout', handler: (ev: MouseEvent) => this.MouseOut.emit(_getEventArg(ev)) },
            { name: 'mouseover', handler: (ev: MouseEvent) => this.MouseOver.emit(_getEventArg(ev)) },
            { name: 'mouseup', handler: (ev: MouseEvent) => this.MouseUp.emit(_getEventArg(ev)) },
            { name: 'rightclick', handler: (ev: MouseEvent) => this.RightClick.emit(_getEventArg(ev)) },
            { name: 'pathchanged', handler: (ev: IPolygonEvent) => this.PathChanged.emit(ev) }
        ];
        handlers.forEach((obj) => {
            const os = this._polygonService.CreateEventObservable(obj.name, this).subscribe(obj.handler);
            this._events.push(os);
        });
    }


    /**
     * Generates IPolygon option changeset from directive settings.
     *
     * @param changes - {@link SimpleChanges} identifying the changes that occured.
     * @returns - {@link IPolygonOptions} containing the polygon options.
     *
     * @memberof MapPolygonDirective
     */
    private GeneratePolygonChangeSet(changes: SimpleChanges): IPolygonOptions {
        const options: IPolygonOptions = { id: this._id };
        let hasOptions: boolean = false;
        if (changes['Clickable']) { options.clickable = this.Clickable; hasOptions = true; }
        if (changes['Draggable']) { options.draggable = this.Draggable; hasOptions = true; }
        if (changes['Editable']) { options.editable = this.Editable; hasOptions = true; }
        if (changes['FillColor'] || changes['FillOpacity']) {
            options.fillColor = this.FillColor;
            options.fillOpacity = this.FillOpacity;
            hasOptions = true;
        }
        if (changes['Geodesic']) { options.geodesic = this.Geodesic; hasOptions = true; }
        if (changes['LabelMaxZoom']) { options.labelMaxZoom = this.LabelMaxZoom; hasOptions = true; }
        if (changes['LabelMinZoom']) { options.labelMinZoom = this.LabelMinZoom; hasOptions = true; }
        if (changes['ShowTooltip']) { options.showTooltip = this.ShowTooltip; hasOptions = true; }
        if (changes['ShowLabel']) { options.showLabel = this.ShowLabel; hasOptions = true; }
        if (changes['StrokeColor'] || changes['StrokeOpacity']) {
            options.strokeColor = this.StrokeColor;
            options.strokeOpacity = this.StrokeOpacity;
            hasOptions = true;
        }
        if (changes['StrokeWeight']) { options.strokeWeight = this.StrokeWeight; hasOptions = true; }
        if (changes['Title']) { options.title = this.Title; hasOptions = true; }
        if (changes['Visible']) { options.visible = this.Visible; hasOptions = true; }
        if (changes['zIndex']) { options.zIndex = this.zIndex; hasOptions = true; }
        return hasOptions ? options : null;
    }

}