<commit_before>///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
///     http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///

import L, {
  FeatureGroup,
  LatLngBounds,
  LatLngTuple,
  markerClusterGroup,
  MarkerClusterGroup,
  MarkerClusterGroupOptions
} from 'leaflet';

import 'leaflet-providers';
import 'leaflet.markercluster/dist/leaflet.markercluster';

import {
  FormattedData,
  MapSettings,
  MarkerSettings,
  PolygonSettings,
  PolylineSettings,
  UnitedMapSettings
} from './map-models';
import { Marker } from './markers';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { filter } from 'rxjs/operators';
import { Polyline } from './polyline';
import { Polygon } from './polygon';
import { createTooltip, parseArray, safeExecute } from '@home/components/widget/lib/maps/maps-utils';
import { WidgetContext } from '@home/models/widget-component.models';
import { DatasourceData } from '@shared/models/widget.models';
import { deepClone, isDefinedAndNotNull } from '@core/utils';

export default abstract class LeafletMap {

    markers: Map<string, Marker> = new Map();
    polylines: Map<string, Polyline> = new Map();
    polygons: Map<string, Polygon> = new Map();
    map: L.Map;
    map$: BehaviorSubject<L.Map> = new BehaviorSubject(null);
    ready$: Observable<L.Map> = this.map$.pipe(filter(map => !!map));
    options: UnitedMapSettings;
    bounds: L.LatLngBounds;
    datasources: FormattedData[];
    markersCluster: MarkerClusterGroup;
    points: FeatureGroup;
    markersData: FormattedData[] = [];
    polygonsData: FormattedData[] = [];

    protected constructor(public ctx: WidgetContext,
                          public $container: HTMLElement,
                          options: UnitedMapSettings) {
        this.options = options;
    }

    public initSettings(options: MapSettings) {
        const { initCallback,
            disableScrollZooming,
            useClusterMarkers,
            zoomOnClick,
            showCoverageOnHover,
            removeOutsideVisibleBounds,
            animate,
            chunkedLoading,
            maxClusterRadius,
            maxZoom }: MapSettings = options;
        if (disableScrollZooming) {
            this.map.scrollWheelZoom.disable();
        }
        if (initCallback) {
            setTimeout(options.initCallback, 0);
        }
        if (useClusterMarkers) {
            const clusteringSettings: MarkerClusterGroupOptions = {
                zoomToBoundsOnClick: zoomOnClick,
                showCoverageOnHover,
                removeOutsideVisibleBounds,
                animate,
                chunkedLoading
            };
            if (maxClusterRadius && maxClusterRadius > 0) {
                clusteringSettings.maxClusterRadius = Math.floor(maxClusterRadius);
            }
            if (maxZoom && maxZoom >= 0 && maxZoom < 19) {
                clusteringSettings.disableClusteringAtZoom = Math.floor(maxZoom);
            }
            this.markersCluster = markerClusterGroup(clusteringSettings);
            this.ready$.subscribe(map => map.addLayer(this.markersCluster));
        }
    }

    addMarkerControl() {
        if (this.options.draggableMarker) {
            let mousePositionOnMap: L.LatLng;
            let addMarker: L.Control;
            this.map.on('mousemove', (e: L.LeafletMouseEvent) => {
                mousePositionOnMap = e.latlng;
            });
            const dragListener = (e: L.DragEndEvent) => {
                if (e.type === 'dragend' && mousePositionOnMap) {
                    const icon = new L.Icon.Default();
                    icon.options.shadowSize = [0, 0];
                    const newMarker = L.marker(mousePositionOnMap, { icon }).addTo(this.map);
                    const datasourcesList = document.createElement('div');
                    const customLatLng = this.convertToCustomFormat(mousePositionOnMap);
                    this.datasources.forEach(ds => {
                        const dsItem = document.createElement('p');
                        dsItem.appendChild(document.createTextNode(ds.entityName));
                        dsItem.setAttribute('style', 'font-size: 14px');
                        dsItem.onclick = () => {
                            const updatedEnttity = { ...ds, ...customLatLng };
                            this.saveMarkerLocation(updatedEnttity).subscribe(() => {
                              this.map.removeLayer(newMarker);
                              this.deleteMarker(ds.entityName);
                              this.createMarker(ds.entityName, updatedEnttity, this.datasources, this.options);
                            });
                        }
                        datasourcesList.append(dsItem);
                    });
                    const deleteBtn = document.createElement('a');
                    deleteBtn.appendChild(document.createTextNode('Delete position'));
                    deleteBtn.setAttribute('color', 'red');
                    deleteBtn.onclick = () => {
                        this.map.removeLayer(newMarker);
                    }
                    datasourcesList.append(deleteBtn);
                    const popup = L.popup();
                    popup.setContent(datasourcesList);
                    newMarker.bindPopup(popup).openPopup();
                }
                addMarker.setPosition('topright')
            }
            L.Control.AddMarker = L.Control.extend({
                onAdd() {
                    const img = L.DomUtil.create('img') as any;
                    img.src = `assets/add_location.svg`;
                    img.style.width = '32px';
                    img.style.height = '32px';
                    img.title = 'Drag and drop to add marker';
                    img.onclick = this.dragMarker;
                    img.draggable = true;
                    const draggableImg = new L.Draggable(img);
                    draggableImg.enable();
                    draggableImg.on('dragend', dragListener)
                    return img;
                },
                onRemove() {
                },
                dragMarker: this.dragMarker
            } as any);
            L.control.addMarker = (opts) => {
                return new L.Control.AddMarker(opts);
            }
            addMarker = L.control.addMarker({ position: 'topright' }).addTo(this.map);
        }
    }

    public setMap(map: L.Map) {
        this.map = map;
        if (this.options.useDefaultCenterPosition) {
            this.map.panTo(this.options.defaultCenterPosition);
            this.bounds = map.getBounds();
        }
        else this.bounds = new L.LatLngBounds(null, null);
        if (this.options.draggableMarker) {
            this.addMarkerControl();
        }
        this.map$.next(this.map);
    }

    public setDataSources(dataSources: FormattedData[]) {
        this.datasources = dataSources;
    }

    public saveMarkerLocation(_e: FormattedData, lat?: number, lng?: number): Observable<any> {
      return of(null);
    }

    createLatLng(lat: number, lng: number): L.LatLng {
        return L.latLng(lat, lng);
    }

    createBounds(): L.LatLngBounds {
        return this.map.getBounds();
    }

    extendBounds(bounds: L.LatLngBounds, polyline: L.Polyline) {
        if (polyline && polyline.getLatLngs() && polyline.getBounds()) {
            bounds.extend(polyline.getBounds());
        }
    }

    invalidateSize() {
        this.map?.invalidateSize(true);
    }

    onResize() {

    }

    getCenter() {
        return this.map.getCenter();
    }

    fitBounds(bounds: LatLngBounds, padding?: LatLngTuple) {
        if (bounds.isValid()) {
            this.bounds = !!this.bounds ? this.bounds.extend(bounds) : bounds;
            if (!this.options.fitMapBounds && this.options.defaultZoomLevel) {
                this.map.setZoom(this.options.defaultZoomLevel, { animate: false });
                if (this.options.useDefaultCenterPosition) {
                    this.map.panTo(this.options.defaultCenterPosition, { animate: false });
                }
                else {
                    this.map.panTo(this.bounds.getCenter());
                }
            } else {
                this.map.once('zoomend', () => {
                    let minZoom = this.options.minZoomLevel;
                    if (this.options.defaultZoomLevel) {
                      minZoom = Math.min(minZoom, this.options.defaultZoomLevel);
                    }
                    if (this.map.getZoom() > minZoom) {
                        this.map.setZoom(minZoom, { animate: false });
                    }
                });
                if (this.options.useDefaultCenterPosition) {
                    this.bounds = this.bounds.extend(this.options.defaultCenterPosition);
                }
                this.map.fitBounds(this.bounds, { padding: padding || [50, 50], animate: false });
                this.map.invalidateSize();
            }
        }
    }

    convertPosition(expression: object): L.LatLng {
        if (!expression) return null;
        const lat = expression[this.options.latKeyName];
        const lng = expression[this.options.lngKeyName];
        if (!isDefinedAndNotNull(lat) || isNaN(lat) || !isDefinedAndNotNull(lng) || isNaN(lng)){
          return null;
        }
        else
            return L.latLng(lat, lng) as L.LatLng;
    }

    convertPositionPolygon(expression: Array<[number, number]>): L.LatLngExpression[] {
      return expression.map((el) => {
        return el.length === 2 && !el.some(isNaN) ? el : null
      }).filter(el => !!el)
    }

    convertToCustomFormat(position: L.LatLng): object {
        return {
            [this.options.latKeyName]: position.lat % 90,
            [this.options.lngKeyName]: position.lng % 180<commit_msg>Refactoring<commit_after>///
/// Copyright © 2016-2020 The Thingsboard Authors
///
/// Licensed under the Apache License, Version 2.0 (the "License");
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
///     http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions and
/// limitations under the License.
///

import L, {
  FeatureGroup,
  LatLngBounds,
  LatLngTuple,
  markerClusterGroup,
  MarkerClusterGroup,
  MarkerClusterGroupOptions
} from 'leaflet';

import 'leaflet-providers';
import 'leaflet.markercluster/dist/leaflet.markercluster';

import {
  FormattedData,
  MapSettings,
  MarkerSettings,
  PolygonSettings,
  PolylineSettings,
  UnitedMapSettings
} from './map-models';
import { Marker } from './markers';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { filter } from 'rxjs/operators';
import { Polyline } from './polyline';
import { Polygon } from './polygon';
import { createTooltip, parseArray, safeExecute } from '@home/components/widget/lib/maps/maps-utils';
import { WidgetContext } from '@home/models/widget-component.models';
import { DatasourceData } from '@shared/models/widget.models';
import { deepClone, isDefinedAndNotNull } from '@core/utils';

export default abstract class LeafletMap {

    markers: Map<string, Marker> = new Map();
    polylines: Map<string, Polyline> = new Map();
    polygons: Map<string, Polygon> = new Map();
    map: L.Map;
    map$: BehaviorSubject<L.Map> = new BehaviorSubject(null);
    ready$: Observable<L.Map> = this.map$.pipe(filter(map => !!map));
    options: UnitedMapSettings;
    bounds: L.LatLngBounds;
    datasources: FormattedData[];
    markersCluster: MarkerClusterGroup;
    points: FeatureGroup;
    markersData: FormattedData[] = [];
    polygonsData: FormattedData[] = [];

    protected constructor(public ctx: WidgetContext,
                          public $container: HTMLElement,
                          options: UnitedMapSettings) {
        this.options = options;
    }

    public initSettings(options: MapSettings) {
        const { initCallback,
            disableScrollZooming,
            useClusterMarkers,
            zoomOnClick,
            showCoverageOnHover,
            removeOutsideVisibleBounds,
            animate,
            chunkedLoading,
            maxClusterRadius,
            maxZoom }: MapSettings = options;
        if (disableScrollZooming) {
            this.map.scrollWheelZoom.disable();
        }
        if (initCallback) {
            setTimeout(options.initCallback, 0);
        }
        if (useClusterMarkers) {
            const clusteringSettings: MarkerClusterGroupOptions = {
                zoomToBoundsOnClick: zoomOnClick,
                showCoverageOnHover,
                removeOutsideVisibleBounds,
                animate,
                chunkedLoading
            };
            if (maxClusterRadius && maxClusterRadius > 0) {
                clusteringSettings.maxClusterRadius = Math.floor(maxClusterRadius);
            }
            if (maxZoom && maxZoom >= 0 && maxZoom < 19) {
                clusteringSettings.disableClusteringAtZoom = Math.floor(maxZoom);
            }
            this.markersCluster = markerClusterGroup(clusteringSettings);
            this.ready$.subscribe(map => map.addLayer(this.markersCluster));
        }
    }

    addMarkerControl() {
        if (this.options.draggableMarker) {
            let mousePositionOnMap: L.LatLng;
            let addMarker: L.Control;
            this.map.on('mousemove', (e: L.LeafletMouseEvent) => {
                mousePositionOnMap = e.latlng;
            });
            const dragListener = (e: L.DragEndEvent) => {
                if (e.type === 'dragend' && mousePositionOnMap) {
                    const icon = new L.Icon.Default();
                    icon.options.shadowSize = [0, 0];
                    const newMarker = L.marker(mousePositionOnMap, { icon }).addTo(this.map);
                    const datasourcesList = document.createElement('div');
                    const customLatLng = this.convertToCustomFormat(mousePositionOnMap);
                    this.datasources.forEach(ds => {
                        const dsItem = document.createElement('p');
                        dsItem.appendChild(document.createTextNode(ds.entityName));
                        dsItem.setAttribute('style', 'font-size: 14px');
                        dsItem.onclick = () => {
                            const updatedEnttity = { ...ds, ...customLatLng };
                            this.saveMarkerLocation(updatedEnttity).subscribe(() => {
                              this.map.removeLayer(newMarker);
                              this.deleteMarker(ds.entityName);
                              this.createMarker(ds.entityName, updatedEnttity, this.datasources, this.options);
                            });
                        }
                        datasourcesList.append(dsItem);
                    });
                    const deleteBtn = document.createElement('a');
                    deleteBtn.appendChild(document.createTextNode('Delete position'));
                    deleteBtn.setAttribute('color', 'red');
                    deleteBtn.onclick = () => {
                        this.map.removeLayer(newMarker);
                    }
                    datasourcesList.append(deleteBtn);
                    const popup = L.popup();
                    popup.setContent(datasourcesList);
                    newMarker.bindPopup(popup).openPopup();
                }
                addMarker.setPosition('topright')
            }
            L.Control.AddMarker = L.Control.extend({
                onAdd() {
                    const img = L.DomUtil.create('img') as any;
                    img.src = `assets/add_location.svg`;
                    img.style.width = '32px';
                    img.style.height = '32px';
                    img.title = 'Drag and drop to add marker';
                    img.onclick = this.dragMarker;
                    img.draggable = true;
                    const draggableImg = new L.Draggable(img);
                    draggableImg.enable();
                    draggableImg.on('dragend', dragListener)
                    return img;
                },
                onRemove() {
                },
                dragMarker: this.dragMarker
            } as any);
            L.control.addMarker = (opts) => {
                return new L.Control.AddMarker(opts);
            }
            addMarker = L.control.addMarker({ position: 'topright' }).addTo(this.map);
        }
    }

    public setMap(map: L.Map) {
        this.map = map;
        if (this.options.useDefaultCenterPosition) {
            this.map.panTo(this.options.defaultCenterPosition);
            this.bounds = map.getBounds();
        }
        else this.bounds = new L.LatLngBounds(null, null);
        if (this.options.draggableMarker) {
            this.addMarkerControl();
        }
        this.map$.next(this.map);
    }

    public setDataSources(dataSources: FormattedData[]) {
        this.datasources = dataSources;
    }

    public saveMarkerLocation(_e: FormattedData, lat?: number, lng?: number): Observable<any> {
      return of(null);
    }

    createLatLng(lat: number, lng: number): L.LatLng {
        return L.latLng(lat, lng);
    }

    createBounds(): L.LatLngBounds {
        return this.map.getBounds();
    }

    extendBounds(bounds: L.LatLngBounds, polyline: L.Polyline) {
        if (polyline && polyline.getLatLngs() && polyline.getBounds()) {
            bounds.extend(polyline.getBounds());
        }
    }

    invalidateSize() {
        this.map?.invalidateSize(true);
    }

    onResize() {

    }

    getCenter() {
        return this.map.getCenter();
    }

    fitBounds(bounds: LatLngBounds, padding?: LatLngTuple) {
        if (bounds.isValid()) {
            this.bounds = !!this.bounds ? this.bounds.extend(bounds) : bounds;
            if (!this.options.fitMapBounds && this.options.defaultZoomLevel) {
                this.map.setZoom(this.options.defaultZoomLevel, { animate: false });
                if (this.options.useDefaultCenterPosition) {
                    this.map.panTo(this.options.defaultCenterPosition, { animate: false });
                }
                else {
                    this.map.panTo(this.bounds.getCenter());
                }
            } else {
                this.map.once('zoomend', () => {
                    let minZoom = this.options.minZoomLevel;
                    if (this.options.defaultZoomLevel) {
                      minZoom = Math.min(minZoom, this.options.defaultZoomLevel);
                    }
                    if (this.map.getZoom() > minZoom) {
                        this.map.setZoom(minZoom, { animate: false });
                    }
                });
                if (this.options.useDefaultCenterPosition) {
                    this.bounds = this.bounds.extend(this.options.defaultCenterPosition);
                }
                this.map.fitBounds(this.bounds, { padding: padding || [50, 50], animate: false });
                this.map.invalidateSize();
            }
        }
    }

    convertPosition(expression: object): L.LatLng {
        if (!expression) return null;
        const lat = expression[this.options.latKeyName];
        const lng = expression[this.options.lngKeyName];
        if (!isDefinedAndNotNull(lat) || isNaN(lat) || !isDefinedAndNotNull(lng) || isNaN(lng)) {
          return null;
        }
        return L.latLng(lat, lng) as L.LatLng;
    }

    convertPositionPolygon(expression: Array<[number, number]>): L.LatLngExpression[] {
      return expression.map((el) => {
        return el.length === 2 && !el.some(isNaN) ? el : null
      }).filter(el => !!el)
    }

    convertToCustomFormat(position: L.LatLng): object {
        return {
            [this.options.latKeyName]: position.lat % 90,
            [this.options.lngKeyName]: position.lng % 180
{'fraction_non_alphanumeric': 0.11293753625991104, 'fraction_numerical': 0.003384258363952814, 'mean_word_length': 1.818503883362856, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 47, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3021413', 'n_tokens_mistral': 5673, 'n_tokens_neox': 5396, 'n_words': 1086}