/** * Created by vitaliy on 1/17/18. (In Onguard IQ) * Updated by Leo * Refactored, typed by Leo and moved to Onguard Components Feb 2021. */ import { GeoJSON, Map, circleMarker, canvas, DomUtil, Util, latLng } from 'leaflet'; import L from 'leaflet'; import { Format } from '../../../../helpers/format.helper'; import { baseOptions, customOptions } from './GeoJson-options'; import { unix } from 'moment'; const TWEEN = require('@tweenjs/tween.js'); export class GeoJsonLayer extends GeoJSON { public options: any; private _layers = {}; private activePie = null; private secondaryActivePie = null; private pointsMap = {}; private featuresMap = {}; private pieSliceLabels = []; private keepCurrentSelection = false; private _animationPropertiesStatic = { offset: 0, resetOffset: 200, repeat: Infinity, yoyo: false, }; private _animationPropertiesDynamic = { duration: null, easingInfo: null, }; private _canvasElement: HTMLCanvasElement; private _animationCanvasElement: HTMLCanvasElement; private _customCanvases: HTMLCanvasElement[]; private originAndDestinationGeoJsonPoints: any; private _animationTween: any; private _animationFrameId: number; public _map: Map; constructor(options = { ...baseOptions, ...customOptions }) { super(); this.options = options; } public initialize = (geoJson: any, options: any) => { // same as L.GeoJSON intialize method, but first performs custom GeoJSON // data parsing and reformatting before finally calling L.GeoJSON addData method options = { ...options, pointToLayer: this.pointToLayer.bind(this), }; // Merge instsance options with default options Util.setOptions(this, options); if (geoJson) { this.setNewData(geoJson); } // establish animation properties using Tween.js library // currently requires the developer to add it to their own app index.html // TODO: find better way to wrap it up in this layer source code // set this._animationPropertiesDynamic.duration value this.setAnimationDuration(this.options.animationDuration); // set this._animationPropertiesDynamic.easingInfo value this.setAnimationEasing( this.options.animationEasingFamily, this.options.animationEasingType, ); // initiate the active animation tween this._animationTween = new TWEEN.Tween(this._animationPropertiesStatic) .to( { offset: this._animationPropertiesStatic.resetOffset, }, this._animationPropertiesDynamic.duration, ) .easing(this._animationPropertiesDynamic.easingInfo.tweenEasingFunction) .repeat(this._animationPropertiesStatic.repeat) .yoyo(this._animationPropertiesStatic.yoyo) .start(); }; public pointToLayer = (geoJsonPoint, latlng) => { const div = new L.DivIcon({ html: '', className: 'div-map-icon', iconSize: new L.Point(60, 60), }); // var marker = new L.CircleMarker(latlng, { // className: 'firefly', // renderer: new L.SVG() // }); const marker = new L.Marker(latlng, { icon: div, }); // if (oms) { // oms.addMarker(marker); // } return marker; }; public onAdd = (map: Map): this => { // call inherited method GeoJSON.prototype.onAdd.call(this, map); // Custom this._map = map; // create new canvas element for manually drawing bezier curves this._canvasElement = this._insertCustomCanvasElement(map, this.options); // create new canvas element for optional, animated bezier curves this._animationCanvasElement = this._insertCustomCanvasElement(map, this.options); this._customCanvases = [this._canvasElement, this._animationCanvasElement]; // establish custom event listeners this.on('click mouseover mouseout', this._modifyInteractionEvent, this); map.on('move', this._resetCanvas, this); map.on('moveend', this._resetCanvasAndWrapGeoJsonCircleMarkers, this); map.on('resize', this._resizeCanvas, this); if (map.options.zoomAnimation && L.Browser.any3d) { map.on('zoomanim', this._animateZoom, this); } // calculate initial size and position of canvas // and draw its content for the first time this._resizeCanvas(); this._resetCanvasAndWrapGeoJsonCircleMarkers(); return this; }; public onRemove = (map: Map): this => { GeoJSON.prototype.onRemove.call(this, map); // Custom Code this._clearCanvas(); this._customCanvases.forEach(function (canvas) { L.DomUtil.remove(canvas); }); // remove custom event listeners this.off('click mouseover mouseout', this._modifyInteractionEvent, this); map.off('move', this._resetCanvas, this); map.off('moveend', this._resetCanvasAndWrapGeoJsonCircleMarkers, this); map.off('resize', this._resizeCanvas, this); if (map.options.zoomAnimation) { map.off('zoomanim', this._animateZoom, this); } return this; }; public setNewData = (geoJsonFeatureCollection: any) => { this.clearLayers(); var newPoints = { type: 'FeatureCollection', features: [], }; if (geoJsonFeatureCollection.features) { geoJsonFeatureCollection.features.forEach((feature, index) => { if ( feature.type === 'Feature' && feature.geometry && feature.geometry.type === 'Point' && feature.geometry.coordinates[0] ) { // origin feature -- modify attribute properties and geometry feature.properties.isOrigin = true; feature.properties.isHovered = false; feature.properties._isSelectedForPathDisplay = true; feature.properties._uniqueId = index + '_origin'; var inbound = 0; var outbound = 0; if (this.options.calculateOriginPieChartByLinkedCharts) { feature.properties.linkedTo.forEach(function (dest) { inbound += dest.inbound; outbound += dest.outbound; }); feature.properties['pieData'] = { inbound: { title: 'test 1', val: inbound, type: 'inbound', }, outbound: { title: 'test 1', val: outbound, type: 'outbound', }, internal: { title: 'test 1', val: feature.properties.internalCalls, type: 'internal', }, }; } feature.properties._volume = this._getPieChartRadius( feature.properties.pieData, ); this.pointsMap[feature.properties.origin_id] = feature.geometry.coordinates; this.featuresMap[feature.properties.origin_id] = feature; newPoints.features.push(feature); } }, this); // all origin/destination features are available for future internal used // but only a filtered subset of these are drawn on the map //this.pieChartsGeoJsonPoints = this._filterGeoJsonPieChartsToDraw(geoJsonFeatureCollection); this.originAndDestinationGeoJsonPoints = newPoints; var geoJsonPointsToDraw = geoJsonFeatureCollection; this.addData(geoJsonPointsToDraw); } else { // TODO: improved handling of invalid incoming GeoJson FeatureCollection? console.warn('Invalid GeoJson'); this.originAndDestinationGeoJsonPoints = null; } this._redrawCanvas(); return this; }; public setSliceTypesToDraw = (config: any) => { this.options.pieChartsDisplayTypes = config; this._redrawCanvas(); }; public setAnimationDuration = (milliseconds) => { milliseconds = Number(milliseconds) || this.options.animationDuration; // change the tween duration on the active animation tween if (this._animationTween) { this._animationTween.to( { offset: this._animationPropertiesStatic.resetOffset, }, milliseconds, ); } this._animationPropertiesDynamic.duration = milliseconds; }; public setAnimationEasing = (easingFamily, easingType) => { var tweenEasingFunction; if ( TWEEN.Easing.hasOwnProperty(easingFamily) && TWEEN.Easing[easingFamily].hasOwnProperty(easingType) ) { tweenEasingFunction = TWEEN.Easing[easingFamily][easingType]; } else { easingFamily = this.options.animationEasingFamily; easingType = this.options.animationEasingType; tweenEasingFunction = TWEEN.Easing[easingFamily][easingType]; } // change the tween easing function on the active animation tween if (this._animationTween) { this._animationTween.easing(tweenEasingFunction); } this._animationPropertiesDynamic.easingInfo = { easingFamily: easingFamily, easingType: easingType, tweenEasingFunction: tweenEasingFunction, }; }; public getAnimationEasingOptions = (prettyPrint): any => { var tweenEasingConsoleOptions = {}; var tweenEasingOptions = {}; Object.keys(TWEEN.Easing).forEach(function (family) { tweenEasingConsoleOptions[family] = { types: Object.keys(TWEEN.Easing[family]).join('", "'), }; tweenEasingOptions[family] = { types: Object.keys(TWEEN.Easing[family]), }; }); if (!!prettyPrint) { console.table(tweenEasingConsoleOptions); } return tweenEasingOptions; }; public playAnimation = () => { this.options.animationStarted = true; this._redrawCanvas(); }; public stopAnimation = () => { this.options.animationStarted = false; this._redrawCanvas(); }; public isFeatureLinked = (activeFeatureUniqueId, targetFeature): boolean => { if (activeFeatureUniqueId === null) { return true; } var linkedDesternations = this.featuresMap[activeFeatureUniqueId]; var result = true; if (linkedDesternations) { result = linkedDesternations.properties.linkedTo.some(function (item) { return item.targetId === targetFeature.properties.origin_id; }); } return result; }; public clearAllPathSelections = (): void => { this.originAndDestinationGeoJsonPoints.features.forEach(function (feature) { feature.properties._isSelectedForPathDisplay = false; }); this._resetCanvas(); }; private _insertCustomCanvasElement = (map: Map, options: any): HTMLCanvasElement => { var canvas = DomUtil.create('canvas', 'leaflet-zoom-animated') as HTMLCanvasElement; const originProp = DomUtil.testProp([ 'transformOrigin', 'WebkitTransformOrigin', 'msTransformOrigin', ]); if (originProp) { canvas.style[originProp] = '50% 50%'; } const pane = map.getPane(options.pane); if (!pane) { console.error(`could not find matching pane`, options.pane); } pane.insertBefore(canvas, pane.firstChild); //@ts-ignore // oms = new OMS_resource.OverlappingMarkerSpiderfier(map, { keepSpiderfied: true }); // oms.addListener('spiderfy', function (markers) { // markers.forEach(marker => { // //Undo selection // if (this.activePie == marker.feature.properties.origin_id) { // this.activePie = null; // } // if (this.secondaryActivePie == marker.feature.properties.origin_id) { // this.secondaryActivePie = -1; // } // // Move Pies and re-draw canvas // var modifiedFeature = this.originAndDestinationGeoJsonPoints.features.find(feature => feature.properties.origin_id == marker.feature.properties.origin_id); // modifiedFeature.geometry.coordinates = [marker._latlng.lng, marker._latlng.lat]; // // Handle Active Pie Children // this.pointsMap[marker.feature.properties.origin_id] = [marker._latlng.lng, marker._latlng.lat]; // }); // this._redrawCanvas(); // }.bind(this)); // oms.addListener('unspiderfy', function (markers) { // markers.forEach(marker => { // // Move Pies and re-draw canvas // var modifiedFeature = this.originAndDestinationGeoJsonPoints.features.find(feature => feature.properties.origin_id == marker.feature.properties.origin_id); // modifiedFeature.geometry.coordinates = [marker._latlng.lng, marker._latlng.lat]; // // Handle Active Pie Children // this.pointsMap[marker.feature.properties.origin_id] = [marker._latlng.lng, marker._latlng.lat]; // }); // this._redrawCanvas(); // }.bind(this)); // //bind oms for use on Leaflet point tn layer function. // L.bind(this); return canvas; }; private _tooltipUpdate = (feature) => { var nameAddition = ''; var items = []; var title; if (this.options.sameSlices) { nameAddition = ' MOS'; } if (this.secondaryActivePie) { var secondaryFeature = this.originAndDestinationGeoJsonPoints.features.find( (feature) => feature.properties.origin_id == this.secondaryActivePie, ); var secondaryLinkedDetails = feature.properties.linkedTo.find( (linkedFeature) => linkedFeature.targetId == this.secondaryActivePie, ); title = feature.properties.facilityName + ' - ' + secondaryFeature.properties.facilityName; if (this.options.pieChartsDisplayTypes['outbound']) { items.push({ name: 'outgoing' + nameAddition, value: secondaryLinkedDetails.inbound, color: this._pieSliceColor('outbound', feature.properties.pieData.outbound.val), }); } if (this.options.pieChartsDisplayTypes['inbound']) { items.push({ name: 'incoming' + nameAddition, value: secondaryLinkedDetails.outbound, color: this._pieSliceColor('inbound', feature.properties.pieData.inbound.val), }); } if (this.options.pieChartsDisplayTypes['internal']) { items.push({ name: 'internal' + nameAddition, value: feature.properties.pieData.internal.val, color: this._pieSliceColor('internal', feature.properties.pieData.internal.val), }); } if (!this.options.sameSlices) { items.push({ name: 'Max Call Starts', value: secondaryLinkedDetails.maxCallStarts ? secondaryLinkedDetails.maxCallStarts.value : '-', color: '#002d5b', secondaryValue: !secondaryLinkedDetails.maxCallStarts ? null : Format.displayDateRanges(secondaryLinkedDetails.maxCallStarts.keys), }); items.push({ name: 'Max Call Volume', value: secondaryLinkedDetails.maxCallVolume ? Format.secondsToHMS(secondaryLinkedDetails.maxCallVolume.value) : '-', color: '#002d5b', }); items.push({ name: '', value: secondaryLinkedDetails.maxCallVolume ? Format.secondsToErlangs(secondaryLinkedDetails.maxCallVolume.value) + ' Erlangs' : '', color: '#002d5b', secondaryValue: !secondaryLinkedDetails.maxCallVolume ? null : Format.displayDateRanges(secondaryLinkedDetails.maxCallVolume.keys), }); items.push({ name: 'Max Concurrent Calls', value: secondaryLinkedDetails.maxConcurrentCalls ? secondaryLinkedDetails.maxConcurrentCalls.value : '-', color: '#002d5b', secondaryValue: !secondaryLinkedDetails.maxConcurrentCalls ? null : Format.displayDateRanges(secondaryLinkedDetails.maxConcurrentCalls.keys), }); items.push({ name: 'Max Bandwidth', value: secondaryLinkedDetails.maxBandwidth ? Format.displayBandwidth(secondaryLinkedDetails.maxBandwidth.value) : '-', color: '#002d5b', secondaryValue: !secondaryLinkedDetails.maxBandwidth ? null : Format.displayDateRanges(secondaryLinkedDetails.maxBandwidth.keys), }); } } else { title = feature.properties.facilityName; if (this.options.pieChartsDisplayTypes['outbound']) { items.push({ name: 'outgoing' + nameAddition, value: feature.properties.pieData.outbound.val, color: this._pieSliceColor('outbound', feature.properties.pieData.outbound.val), }); } if (this.options.pieChartsDisplayTypes['inbound']) { items.push({ name: 'incoming' + nameAddition, value: feature.properties.pieData.inbound.val, color: this._pieSliceColor('inbound', feature.properties.pieData.inbound.val), }); } if (this.options.pieChartsDisplayTypes['internal']) { items.push({ name: 'internal' + nameAddition, value: feature.properties.pieData.internal.val, color: this._pieSliceColor('internal', feature.properties.pieData.internal.val), }); } if (!this.options.sameSlices) { items.push({ name: 'Max Call Starts', value: feature.properties.maxCallStarts ? feature.properties.maxCallStarts.value : '-', color: '#002d5b', secondaryValue: !feature.properties.maxCallStarts ? null : Format.displayDateRanges(feature.properties.maxCallStarts.keys), }); items.push({ name: 'Max Call Volume', value: feature.properties.maxCallVolume ? Format.secondsToHMS(feature.properties.maxCallVolume.value) : '-', color: '#002d5b', }); items.push({ name: '', value: feature.properties.maxCallVolume ? Format.secondsToErlangs(feature.properties.maxCallVolume.value) + ' Erlangs' : '-', color: '#002d5b', secondaryValue: !feature.properties.maxCallVolume ? null : Format.displayDateRanges(feature.properties.maxCallVolume.keys), }); items.push({ name: 'Max Concurrent Calls', value: feature.properties.maxConcurrentCalls ? feature.properties.maxConcurrentCalls.value : '-', color: '#002d5b', secondaryValue: !feature.properties.maxConcurrentCalls ? null : Format.displayDateRanges(feature.properties.maxConcurrentCalls.keys), }); items.push({ name: 'Max Bandwidth', value: feature.properties.maxBandwidth ? Format.displayBandwidth(feature.properties.maxBandwidth.value) : '-', color: '#002d5b', secondaryValue: !feature.properties.maxBandwidth ? null : Format.displayDateRanges(feature.properties.maxBandwidth.keys), }); } } var dataPoint = { title: title, items: items, }; }; private _modifyInteractionEvent = (e) => { if (e.type == 'click') { // nullify secondary pie if it is clicked on if (this.secondaryActivePie == e.layer.feature.properties.origin_id) { this.secondaryActivePie = null; var activeFeature = this.originAndDestinationGeoJsonPoints.features.find( (feature) => feature.properties.origin_id == this.activePie, ); this._tooltipUpdate(activeFeature); this._redrawCanvas(); } // This is set to disable the selection when expanding clusters else if (this.secondaryActivePie < 0) { this.secondaryActivePie = null; } // not clicking on the main pie set clicked pie as secondary active else if (this.activePie != e.layer.feature.properties.origin_id && this.activePie) { // A secondary Pie can only be set if it is linked to the primary pie var activeFeature = this.originAndDestinationGeoJsonPoints.features.find( (feature) => feature.properties.origin_id == this.activePie, ); if ( activeFeature.properties.linkedTo.find( (feature) => feature.targetId == e.layer.feature.properties.origin_id, ) ) { this.secondaryActivePie = e.layer.feature.properties.origin_id; this._tooltipUpdate(activeFeature); this._redrawCanvas(); } } // otherwise the active pie was clicked and we will toggle the keep selection option. else if (this.activePie) { this.keepCurrentSelection = !this.keepCurrentSelection; // the secondary pie should not persist if the primary pie is selected or deselected this.secondaryActivePie = null; } } else if (e.type == 'mouseover' && this.keepCurrentSelection === false) { this.activePie = e.layer.feature.properties.origin_id; this._tooltipUpdate(e.layer.feature); this._redrawCanvas(); } else if (e.type == 'mouseout' && this.keepCurrentSelection === false) { this.activePie = null; setTimeout(() => { console.log('received.data.map_data.hide'); }, 100); this._redrawCanvas(); } }; private _animateZoom = (e: L.ZoomAnimEvent) => { // if (!this.activePie) { // oms.unspiderfy(); // } // oms.unspiderfy(); const scale = this._map.getZoomScale(e.zoom); const eventCenter = this._map.latLngToLayerPoint(e.center); const mapCenter = this._map.latLngToLayerPoint(this._map.getCenter()); const offset = eventCenter.subtract(mapCenter).multiplyBy(-scale); // Subtract pane Position const pane = this._map.getPane(this.options.pane).getBoundingClientRect(); const panePosition = new L.Point(pane.x / 2, pane.y / 2); offset.subtract(panePosition); // const offset = e.center._multiplyBy(-scale).subtract(this._map.getCenter()); // const offset = this._map.getCenter(e.center)._multiplyBy(-scale).subtract(this._map._getMapPanePos()); if (L.DomUtil.setTransform) { this._customCanvases.forEach(function (canvas) { L.DomUtil.setTransform(canvas, offset, scale); }); } }; private _resizeCanvas = () => { // update the canvas size var size = this._map.getSize(); this._customCanvases.forEach(function (canvas) { canvas.width = size.x; canvas.height = size.y; }); this._resetCanvas(); }; private _drawPieSlice = (ctx, point, radius, startAngle, sliceAngle, color, text, type) => { var endAngle = startAngle + sliceAngle; // Draw shadow on the background circle if (type == '') { ctx.shadowColor = 'black'; ctx.shadowBlur = 1; } ctx.fillStyle = color; ctx.beginPath(); ctx.moveTo(point.x, point.y); ctx.arc(point.x, point.y, radius, startAngle, endAngle); ctx.closePath(); ctx.fill(); // reset shadow to 0 incase it was set ctx.shadowBlur = 0; ctx.font = this.options.pieChartSliceFontSize + 'px ' + this.options.pieChartSliceFont; ctx.fillStyle = '#ffffff'; var a = (endAngle - startAngle) / 2 + startAngle; var textrX = 0; var textrY = 0; ctx.fillStyle = '#FFFFFF'; ctx.font = '10px Arial'; if (2 * Math.PI == endAngle - startAngle) { textrX = point.x; textrY = point.y; //ctx.fillStyle = '#FFFFFF'; //ctx.font = "10px Arial"; //ctx.fillText(type.toUpperCase(), textrX - (ctx.measureText(type.toUpperCase()).width / 2), textrY); this.pieSliceLabels.push({ x: textrX - ctx.measureText(type.toUpperCase()).width / 2, y: textrY, text: type.toUpperCase(), fillStyle: '#FFFFFF', font: '10px Arial', }); textrY = point.y + 5; } else { textrX = point.x + radius * 0.55 * Math.cos(a); textrY = point.y + radius * 0.55 * Math.sin(a); } // ctx.fillText(text, textrX - (ctx.measureText(text).width / 2), textrY + (this.options.pieChartSliceFontSize / 2)); ctx.font = this.options.pieChartSliceFontSize + 'px ' + this.options.pieChartSliceFont; this.pieSliceLabels.push({ x: textrX - ctx.measureText(text).width / 2, y: textrY + this.options.pieChartSliceFontSize / 2, text: text, fillStyle: '#FFFFFF', font: this.options.pieChartSliceFontSize + 'px ' + this.options.pieChartSliceFont, }); // Add INC, OUT to if (this.options.sameSlices && type != 'internal') { textrX = point.x + radius * 0.55 * Math.cos(a); textrY = point.y - 10 + radius * 0.55 * Math.sin(a); var label = ''; if (type == 'inbound') { label = 'INC'; } else if (type == 'outbound') { label = 'OUT'; } ctx.font = this.options.pieChartSliceTinyFontSize + 'px ' + this.options.pieChartSliceFont; this.pieSliceLabels.push({ x: textrX - ctx.measureText(text).width / 2, y: textrY + this.options.pieChartSliceTinyFontSize / 2, text: label, fillStyle: '#FFFFFF', font: this.options.pieChartSliceTinyFontSize + 'px ' + this.options.pieChartSliceFont, }); } return endAngle; }; private _drawPieSlicesLabels = (ctx) => { this.pieSliceLabels.forEach(function (item) { ctx.fillStyle = item.fillStyle; ctx.font = item.font; ctx.fillText(item.text, item.x, item.y); }); this.pieSliceLabels = []; }; private _getPieChartRadius = (items) => { var minsize = 15; var maxsize = 30; var val = maxsize; if (items.internal && items.inbound && items.outbound) { val = items.internal.val + items.inbound.val + items.outbound.val; } if (val < minsize) { val = minsize; } if (val > maxsize) { val = maxsize; } return val; }; private _sliceAngle = (sliceStep, sliceVal, totalValue, enabledCount = 0) => { var angle = 2 * Math.PI * ((this.options.sameSlices ? sliceStep : sliceVal) / totalValue); if (sliceVal == totalValue && totalValue == 0 && this.options.sameSlices) { if (enabledCount == 1) { return 2 * Math.PI; } else if (enabledCount == 2) { return Math.PI; } } return angle; }; private _sliceValueLabel = (sliceVal, enablesSlices) => { if (this.options.sameSlices) { return sliceVal; } return sliceVal == 0 && enablesSlices > 1 ? '' : sliceVal; }; private _pieSliceColor = (type, value) => { if (this.options.sameSlices) { if (value == 0) { return '#002d5b'; } return this.options.callQualityColors.find((colorOption) => { return colorOption.max >= value; }).color; } else { return this.options.pieChartsColors[type]; } }; private _lineColor = (color, value) => { if (this.options.sameSlices) { if (value == 0) { return '#002d5b'; } return this.options.callQualityColors.find((colorOption) => { return colorOption.max >= value; }).color; } else { return color; } }; private _drawPieChart = (options) => { var pieChartsDisplayTypes = Object.assign({}, this.options.pieChartsDisplayTypes); var totalValue = 0; options.ctx.fillStyle = '#FF9733'; options.ctx.beginPath(); options.ctx.moveTo(options.point.x, options.point.y); var startAngle = (-90 * Math.PI) / 180; //options.ctx.globalAlpha = 1; var displayType = 'internal'; // draw blanc grey circle this._drawPieSlice( options.ctx, options.point, options.volume, 0, 2 * Math.PI, '#002d5b', '', '', ); if (options.selected) { totalValue = 0; if (this.options.pieChartsDisplayTypes['internal']) { this._drawPieSlice( options.ctx, options.point, options.volume, 0, 2 * Math.PI, this._pieSliceColor('internal', options.pieData.internal.val), options.pieData.internal.val, 'internal', ); } Object.keys(pieChartsDisplayTypes).forEach(function (key) { pieChartsDisplayTypes[key] = key == displayType; }); var textrX = options.point.x; var textrY = options.point.y - 60; options.ctx.fillStyle = '#08315A'; options.ctx.font = '20px Arial'; options.ctx.fillText( options.facilityName, textrX - options.ctx.measureText(options.facilityName).width / 2, textrY, ); } else if (options.active) { totalValue = 0; var enablesSlices = 0; let outboundValue = 0; let inboundValue = 0; if (this.options.pieChartsDisplayTypes['inbound']) { totalValue += options.pieData.inbound.val; enablesSlices++; } if (this.options.pieChartsDisplayTypes['outbound']) { totalValue += options.pieData.outbound.val; enablesSlices++; } if ( options.pieData.inbound.val == options.pieData.outbound.val && options.pieData.outbound.val == 0 ) { outboundValue = 0; inboundValue = 0; if ( this.options.pieChartsDisplayTypes['inbound'] && this.options.pieChartsDisplayTypes['outbound'] ) { totalValue = 2; options.pieData.inbound.val = 1; options.pieData.outbound.val = 1; } else if (this.options.pieChartsDisplayTypes['inbound']) { totalValue = 1; options.pieData.inbound.val = 1; options.pieData.outbound.val = 0; } else if (this.options.pieChartsDisplayTypes['outbound']) { totalValue = 1; options.pieData.inbound.val = 0; options.pieData.outbound.val = 1; } } else { inboundValue = this._sliceValueLabel(options.pieData.inbound.val, enablesSlices); outboundValue = this._sliceValueLabel(options.pieData.outbound.val, enablesSlices); } sliceStep = totalValue / enablesSlices; if (this.options.pieChartsDisplayTypes['inbound']) { startAngle = this._drawPieSlice( options.ctx, options.point, options.volume, startAngle, this._sliceAngle(sliceStep, options.pieData.inbound.val, totalValue), this._pieSliceColor('inbound', options.pieData.inbound.val), inboundValue, 'inbound', ); } if (this.options.pieChartsDisplayTypes['outbound']) { startAngle = this._drawPieSlice( options.ctx, options.point, options.volume, startAngle, this._sliceAngle(sliceStep, options.pieData.outbound.val, totalValue), this._pieSliceColor('outbound', options.pieData.outbound.val), outboundValue, 'outbound', ); } } else { totalValue = 0; var enablesSlices = 0; if (this.options.pieChartsDisplayTypes['internal']) { totalValue += options.pieData.internal.val; enablesSlices++; } if (this.options.pieChartsDisplayTypes['inbound']) { totalValue += options.pieData.inbound.val; enablesSlices++; } if (this.options.pieChartsDisplayTypes['outbound']) { totalValue += options.pieData.outbound.val; enablesSlices++; } var sliceStep = totalValue / enablesSlices; if (this.options.pieChartsDisplayTypes['internal']) { startAngle = this._drawPieSlice( options.ctx, options.point, options.volume, startAngle, this._sliceAngle( sliceStep, options.pieData.internal.val, totalValue, enablesSlices, ), this._pieSliceColor('internal', options.pieData.internal.val), this._sliceValueLabel(options.pieData.internal.val, enablesSlices), 'internal', ); } if (this.options.pieChartsDisplayTypes['inbound']) { startAngle = this._drawPieSlice( options.ctx, options.point, options.volume, startAngle, this._sliceAngle( sliceStep, options.pieData.inbound.val, totalValue, enablesSlices, ), this._pieSliceColor('inbound', options.pieData.inbound.val), this._sliceValueLabel(options.pieData.inbound.val, enablesSlices), 'inbound', ); } if (this.options.pieChartsDisplayTypes['outbound']) { this._drawPieSlice( options.ctx, options.point, options.volume, startAngle, this._sliceAngle( sliceStep, options.pieData.outbound.val, totalValue, enablesSlices, ), this._pieSliceColor('outbound', options.pieData.outbound.val), this._sliceValueLabel(options.pieData.outbound.val, enablesSlices), 'outbound', ); } } //draw all labels at the end of all other elements this._drawPieSlicesLabels(options.ctx); }; private _resetCanvas = (): void => { // update the canvas position and redraw its content var topLeft = this._map.containerPointToLayerPoint([0, 0]); this._customCanvases.forEach(function (canvas) { DomUtil.setPosition(canvas, topLeft); }); this._redrawCanvas(); }; private _resetCanvasAndWrapGeoJsonCircleMarkers = () => { this._resetCanvas(); // Leaflet will redraw a CircleMarker when its latLng is changed // sometimes they are drawn 2+ times if this occurs during many "move" events // so for now, only chang CircleMarker latlng after a single "moveend" event // this._wrapGeoJsonCircleMarkers(); }; private _animator = (time = 100) => { this._animationCanvasElement .getContext('2d') .clearRect( 0, 0, this._animationCanvasElement.width, this._animationCanvasElement.height, ); this._drawSelectedCanvasPaths(true, false); // draw it again to give the appearance of a moving dot with a new lineDashOffset TWEEN.update(time); this._animationFrameId = L.Util.requestAnimFrame(this._animator, this); }; private _redrawCanvas = () => { // draw canvas content (only the Bezier curves) if (this._map && this.originAndDestinationGeoJsonPoints) { this._clearCanvas(); // loop over each of the "selected" features and re-draw the canvas paths if (this._animationFrameId) { Util.cancelAnimFrame(this._animationFrameId); } if (this.options.animationStarted) { // start animation loop this._animator(); } this._drawSelectedCanvasPaths(false, false); //draw pie charts separatly to prevent overlaps by other canvas elments this._drawSelectedCanvasPaths(false, true); } }; private _clearCanvas = () => { this._customCanvases.forEach(function (canvas) { canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height); }); if (this._animationFrameId) { Util.cancelAnimFrame(this._animationFrameId); } }; public mapValues = (x, in_min, in_max, out_min, out_max) => { in_max = this.options.mapValueMaxRange; var out = ((x - in_min) * (out_max - out_min)) / (in_max - in_min) + out_min; if (out > out_max) { return out_max; } return out; }; public getSinglePoint = (origin) => { var originLatLng = this._wrapAroundLatLng(latLng(origin)); return originLatLng; }; public getOriginDestinationPoints = (origin, destination, volumeUp, volumeDown) => { var screenOriginPoint = this._map.latLngToContainerPoint(this.getSinglePoint(origin)); var screenDestinationPoint = this._map.latLngToContainerPoint( this.getSinglePoint(destination), ); return { screenOriginPointUp: screenOriginPoint, screenOriginPointDown: screenOriginPoint, screenDestinationPointUp: screenDestinationPoint, screenDestinationPointDown: screenDestinationPoint, screenOriginPoint: screenOriginPoint, screenDestinationPoint: screenDestinationPoint, }; }; private _drawSelectedCanvasPaths = (animate, onlyPieCharts) => { if (animate == false && onlyPieCharts == true) { } var ctx = animate ? this._animationCanvasElement.getContext('2d') : this._canvasElement.getContext('2d'); var originAndDestinationFieldIds = this.options.originAndDestinationFieldIds; this.originAndDestinationGeoJsonPoints.features.forEach(function (feature) { var originXCoordinate = feature.geometry.coordinates[0]; var originYCoordinate = feature.geometry.coordinates[1]; var originAndDesternationPointsd = this.getOriginDestinationPoints( [originYCoordinate, originXCoordinate], [0, 0], 0, 10, ); var hovered = this.activePie == feature.properties.origin_id; if (onlyPieCharts == true && animate == false) { var screenOriginPoint = this._map.latLngToContainerPoint( this.getSinglePoint([originYCoordinate, originXCoordinate]), ); // if there is an active pie if (this.activePie) { // if the current feature iteration is the active pie if (this.activePie == feature.properties.origin_id) { // Draw opaque pies first ctx.globalAlpha = 0.4; this.originAndDestinationGeoJsonPoints.features.forEach((feature) => { var originXCoordinate = feature.geometry.coordinates[0]; var originYCoordinate = feature.geometry.coordinates[1]; var originAndDesternationPointsd = this.getOriginDestinationPoints( [originYCoordinate, originXCoordinate], [0, 0], 0, 10, ); var pieChartOptions = { facilityName: feature.properties.facilityName, selected: this.activePie == feature.properties.origin_id, usState: feature.properties.usState, active: false, pieData: feature.properties.pieData, ctx: ctx, point: originAndDesternationPointsd.screenOriginPoint, volume: feature.properties._volume, }; this._drawPieChart(pieChartOptions); }); ctx.globalAlpha = 1.0; // only draw primary and secondary pies if secondaryActivePie is defined if (this.secondaryActivePie) { var secondaryFeature = this.originAndDestinationGeoJsonPoints.features.find( (feature) => feature.properties.origin_id == this.secondaryActivePie, ); var secondaryLinkedDetails = feature.properties.linkedTo.find( (linkedFeature) => linkedFeature.targetId == this.secondaryActivePie, ); var originXCoordinate = secondaryFeature.geometry.coordinates[0]; var originYCoordinate = secondaryFeature.geometry.coordinates[1]; var originAndDesternationPointsd = this.getOriginDestinationPoints( [originYCoordinate, originXCoordinate], [0, 0], 0, 10, ); let pieData = { inbound: { title: 'test 1', val: secondaryLinkedDetails.outbound, type: 'inbound', }, outbound: { title: 'test 1', val: secondaryLinkedDetails.inbound, type: 'outbound', }, internal: { title: 'test 1', val: secondaryLinkedDetails.outbound, type: 'outbound', }, }; let pieChartOptions = { facilityName: secondaryFeature.properties.facilityName, selected: this.activePie == secondaryFeature.properties.origin_id, usState: secondaryFeature.properties.usState, active: true, pieData: pieData, ctx: ctx, point: originAndDesternationPointsd.screenOriginPoint, volume: this._getPieChartRadius(pieData), }; this._drawPieChart(pieChartOptions); } // otherwise draw all pies linked ot activePie else { // Draw Linked to First so active pie is drawn on top. feature.properties.linkedTo.forEach((des) => { var symbol = this._getSymbolProperties( feature, this.options.canvasBezierStyle, ); var destinationFeature = this.featuresMap[des.targetId]; let pieData = { inbound: { title: 'test 1', val: des.outbound, type: 'inbound', }, outbound: { title: 'test 1', val: des.inbound, type: 'outbound', }, internal: { title: 'test 1', val: des.outbound, type: 'outbound', }, }; if (!this.pointsMap[des.targetId]) { return; } var destinationXCoordinate = this.pointsMap[des.targetId][0]; var destinationYCoordinate = this.pointsMap[des.targetId][1]; var originAndDesternationPoints = this.getOriginDestinationPoints( [originYCoordinate, originXCoordinate], [destinationYCoordinate, destinationXCoordinate], des.outbound, des.inbound, ); let pieChartOptions = { facilityName: destinationFeature.properties.facilityName, selected: false, usState: destinationFeature.properties.usState, active: true, pieData: pieData, ctx: ctx, point: originAndDesternationPoints.screenDestinationPoint, volume: this._getPieChartRadius(pieData), }; this._drawPieChart(pieChartOptions); }); } // Now draw active pie on top var pieChartOptions = { facilityName: feature.properties.facilityName, selected: this.activePie == feature.properties.origin_id, usState: feature.properties.usState, active: true, pieData: feature.properties.pieData, ctx: ctx, point: screenOriginPoint, volume: feature.properties._volume, }; this._drawPieChart(pieChartOptions); } } else { var pieChartOptions = { facilityName: feature.properties.facilityName, selected: this.activePie == feature.properties.origin_id, usState: feature.properties.usState, active: false, pieData: feature.properties.pieData, ctx: ctx, point: originAndDesternationPointsd.screenOriginPoint, volume: feature.properties._volume, }; this._drawPieChart(pieChartOptions); } } else { // This is where LINES are drawn if (feature.properties._isSelectedForPathDisplay) { if (animate) { ctx.globalAlpha = 0.7; if ( hovered && feature.properties.linkedTo && feature.properties.linkedTo.length ) { var symbol = this._getSymbolProperties( feature, this.options.canvasBezierStyle, ); // Only draw lines between activePie and secondaryActivePie if secondaryActivePie is defined if (this.secondaryActivePie) { var des = feature.properties.linkedTo.find( (linkedPie) => linkedPie.targetId == this.secondaryActivePie, ); var symbolUp; var symbolDown; var destinationXCoordinate = this.pointsMap[des.targetId][0]; var destinationYCoordinate = this.pointsMap[des.targetId][1]; var originXCoordinate = feature.geometry.coordinates[0]; var originYCoordinate = feature.geometry.coordinates[1]; var originAndDesternationPoints = this.getOriginDestinationPoints( [originYCoordinate, originXCoordinate], [destinationYCoordinate, destinationXCoordinate], des.outbound, des.inbound, ); symbol = this._getSymbolProperties( feature, this.options.animatedCanvasBezierStyle, ); symbolUp = Object.assign( {}, symbol, { lineWidth: this.options.sameSlices ? 10 : des.inbound, value: des.inbound, }, this.options.animatedCanvasBezierStyleUp || {}, ); symbolDown = Object.assign( {}, symbol, { lineWidth: this.options.sameSlices ? 10 : des.outbound, value: des.outbound, }, this.options.animatedCanvasBezierStyleDown || {}, ); if (this.options.pieChartsDisplayTypes['outbound']) { ctx.beginPath(); this._animateCanvasLineSymbol( ctx, symbolDown, originAndDesternationPoints.screenOriginPointDown, originAndDesternationPoints.screenDestinationPointDown, true, ); ctx.stroke(); ctx.closePath(); } if (this.options.pieChartsDisplayTypes['inbound']) { ctx.beginPath(); this._animateCanvasLineSymbol( ctx, symbolUp, originAndDesternationPoints.screenOriginPointUp, originAndDesternationPoints.screenDestinationPointUp, false, ); ctx.stroke(); ctx.closePath(); } } else { feature.properties.linkedTo.forEach((des) => { var symbolUp; var symbolDown; if (!this.pointsMap[des.targetId]) { return; } var destinationXCoordinate = this.pointsMap[des.targetId][0]; var destinationYCoordinate = this.pointsMap[des.targetId][1]; var originXCoordinate = feature.geometry.coordinates[0]; var originYCoordinate = feature.geometry.coordinates[1]; var originAndDesternationPoints = this.getOriginDestinationPoints( [originYCoordinate, originXCoordinate], [destinationYCoordinate, destinationXCoordinate], des.outbound, des.inbound, ); symbol = this._getSymbolProperties( feature, this.options.animatedCanvasBezierStyle, ); symbolUp = Object.assign( {}, symbol, { lineWidth: this.options.sameSlices ? 10 : des.inbound, value: des.inbound, }, this.options.animatedCanvasBezierStyleUp || {}, ); symbolDown = Object.assign( {}, symbol, { lineWidth: this.options.sameSlices ? 10 : des.outbound, value: des.outbound, }, this.options.animatedCanvasBezierStyleDown || {}, ); if (this.options.pieChartsDisplayTypes['outbound']) { ctx.beginPath(); this._animateCanvasLineSymbol( ctx, symbolDown, originAndDesternationPoints.screenOriginPointDown, originAndDesternationPoints.screenDestinationPointDown, true, ); ctx.stroke(); ctx.closePath(); } if (this.options.pieChartsDisplayTypes['inbound']) { ctx.beginPath(); this._animateCanvasLineSymbol( ctx, symbolUp, originAndDesternationPoints.screenOriginPointUp, originAndDesternationPoints.screenDestinationPointUp, false, ); ctx.stroke(); ctx.closePath(); } }); } } } else { if ( hovered && feature.properties.linkedTo && feature.properties.linkedTo.length ) { var symbol = this._getSymbolProperties( feature, this.options.canvasBezierStyle, ); // Only draw path between primary and secondary pies is secondary is defined if (this.secondaryActivePie) { var des = feature.properties.linkedTo.find( (linkedPie) => linkedPie.targetId == this.secondaryActivePie, ); var symbolUp; var symbolDown; var destinationXCoordinate = this.pointsMap[des.targetId][0]; var destinationYCoordinate = this.pointsMap[des.targetId][1]; var originAndDesternationPoints = this.getOriginDestinationPoints( [originYCoordinate, originXCoordinate], [destinationYCoordinate, destinationXCoordinate], des.outbound, des.inbound, ); symbolUp = Object.assign( {}, symbol, { lineWidth: this.options.sameSlices ? 10 : des.inbound, value: des.inbound, }, this.options.canvasBezierStyleUp || {}, ); symbolDown = Object.assign( {}, symbol, { lineWidth: this.options.sameSlices ? 10 : des.outbound, value: des.outbound, }, this.options.canvasBezierStyleDown || {}, ); ctx.globalAlpha = 1; if (this.options.pieChartsDisplayTypes['inbound']) { ctx.beginPath(); this._applyAnimatedCanvasLineSymbol( ctx, symbolUp, originAndDesternationPoints.screenOriginPointUp, originAndDesternationPoints.screenDestinationPointUp, false, ); ctx.stroke(); ctx.closePath(); } if (this.options.pieChartsDisplayTypes['outbound']) { ctx.beginPath(); this._applyAnimatedCanvasLineSymbol( ctx, symbolDown, originAndDesternationPoints.screenOriginPointDown, originAndDesternationPoints.screenDestinationPointDown, true, ); ctx.stroke(); ctx.closePath(); } } // Otherwise draw paths to all pies linked to activePie else { feature.properties.linkedTo.forEach((des) => { var symbolUp; var symbolDown; if (!this.pointsMap[des.targetId]) { return; } var destinationXCoordinate = this.pointsMap[des.targetId][0]; var destinationYCoordinate = this.pointsMap[des.targetId][1]; var originAndDesternationPoints = this.getOriginDestinationPoints( [originYCoordinate, originXCoordinate], [destinationYCoordinate, destinationXCoordinate], des.outbound, des.inbound, ); symbolUp = Object.assign( {}, symbol, { lineWidth: this.options.sameSlices ? 10 : des.inbound, value: des.inbound, }, this.options.canvasBezierStyleUp || {}, ); symbolDown = Object.assign( {}, symbol, { lineWidth: this.options.sameSlices ? 10 : des.outbound, value: des.outbound, }, this.options.canvasBezierStyleDown || {}, ); ctx.globalAlpha = 0.7; if (this.options.pieChartsDisplayTypes['inbound']) { ctx.beginPath(); this._applyAnimatedCanvasLineSymbol( ctx, symbolUp, originAndDesternationPoints.screenOriginPointUp, originAndDesternationPoints.screenDestinationPointUp, false, ); ctx.stroke(); ctx.closePath(); } if (this.options.pieChartsDisplayTypes['outbound']) { ctx.beginPath(); this._applyAnimatedCanvasLineSymbol( ctx, symbolDown, originAndDesternationPoints.screenOriginPointDown, originAndDesternationPoints.screenDestinationPointDown, true, ); ctx.stroke(); ctx.closePath(); } }); } } // this._drawPieChart(ctx, originAndDesternationPointsd.screenOriginPoint, feature); } } } }, this); }; private _getSymbolProperties = (feature, canvasSymbolConfig) => { // get the canvas symbol properties var symbol; var filteredSymbols; if (canvasSymbolConfig.type === 'simple') { symbol = canvasSymbolConfig.symbol; } else if (canvasSymbolConfig.type === 'uniqueValue') { filteredSymbols = canvasSymbolConfig.uniqueValueInfos.filter(function (info) { return info.value === feature.properties[canvasSymbolConfig.field]; }); symbol = filteredSymbols[0].symbol; } else if (canvasSymbolConfig.type === 'classBreaks') { filteredSymbols = canvasSymbolConfig.classBreakInfos.filter(function (info) { return ( info.classMinValue <= feature.properties[canvasSymbolConfig.field] && info.classMaxValue >= feature.properties[canvasSymbolConfig.field] ); }); if (filteredSymbols.length) { symbol = filteredSymbols[0].symbol; } else { symbol = canvasSymbolConfig.defaultSymbol; } } return symbol; }; private _applyAnimatedCanvasLineSymbol = ( ctx, symbolObject, origin, destination, isOutbound, ) => { ctx.lineCap = symbolObject.lineCap; ctx.lineWidth = this.options.sameSlices ? 10 : this.mapValues(symbolObject.lineWidth, 0, 2000, 5, 25); ctx.strokeStyle = this._lineColor(symbolObject.strokeStyle, symbolObject.value); ctx.shadowBlur = symbolObject.shadowBlur; ctx.shadowColor = symbolObject.shadowColor; const [shiftedOrigin, pivot1, pivot2, shiftedDestination] = this._findBeizerPoints( origin, destination, ctx.lineWidth, isOutbound, ); ctx.moveTo(shiftedOrigin.x, shiftedOrigin.y); ctx.bezierCurveTo( pivot1.x, pivot1.y, pivot2.x, pivot2.y, shiftedDestination.x, shiftedDestination.y, ); }; private _animateCanvasLineSymbol = (ctx, symbolObject, origin, destination, isOutbound) => { ctx.lineCap = symbolObject.lineCap; ctx.lineWidth = this.options.sameSlices ? 10 : this.mapValues(symbolObject.lineWidth, 0, 2000, 4, 24); ctx.strokeStyle = this._lineColor(symbolObject.strokeStyle, symbolObject.value); ctx.fill = this._lineColor(symbolObject.strokeStyle, symbolObject.value); ctx.shadowBlur = symbolObject.shadowBlur; ctx.shadowColor = symbolObject.shadowColor; // ctx.setLineDash([symbolObject.lineDashOffsetSize, (this._animationPropertiesStatic.resetOffset - symbolObject.lineDashOffsetSize * 10)/ 10]); ctx.setLineDash([10, 10]); if (isOutbound) { ctx.lineDashOffset = -this._animationPropertiesStatic.offset; // this makes the dot appear to move when the entire top canvas is redrawn } else { ctx.lineDashOffset = +this._animationPropertiesStatic.offset; // this makes the dot appear to move when the entire top canvas is redrawn } const wholeWidth = this.mapValues(symbolObject.lineWidth, 0, 2000, 5, 25); const [shiftedOrigin, pivot1, pivot2, shiftedDestination] = this._findBeizerPoints( origin, destination, wholeWidth, isOutbound, ); ctx.moveTo(shiftedOrigin.x, shiftedOrigin.y); ctx.bezierCurveTo( pivot1.x, pivot1.y, pivot2.x, pivot2.y, shiftedDestination.x, shiftedDestination.y, ); }; private _findBeizerPoints(origin, destination, lineWidth, direction) { // Get orig - dest vector (unit) const directionVector = { x: destination.x - origin.x, y: destination.y - origin.y, }; // Use Dot Product to find perpendicular Vector const perpendicular = { x: -(directionVector.y / directionVector.x), y: 1, }; const magnitude = Math.sqrt(Math.pow(perpendicular.x, 2) + Math.pow(perpendicular.y, 2)); const unit = { x: (perpendicular.x / magnitude) * ((lineWidth * 2) / 3), y: (perpendicular.y / magnitude) * ((lineWidth * 2) / 3), }; if (direction) { unit.x *= -1; unit.y *= -1; } const shiftedOrigin = { x: origin.x + unit.x, y: origin.y + unit.y, }; const shiftedDestination = { x: destination.x + unit.x, y: destination.y + unit.y, }; const center = { x: (shiftedOrigin.x + shiftedDestination.x) / 2, y: (shiftedOrigin.y + shiftedDestination.y) / 2, }; const pivot1 = { x: shiftedOrigin.x, y: center.y, }; const pivot2 = { x: shiftedDestination.x, y: center.y, }; return [shiftedOrigin, pivot1, pivot2, shiftedDestination]; } private _wrapGeoJsonCircleMarkers = () => {}; private _wrapAroundLatLng = (latLng) => { if (this._map && this.options.wrapAroundCanvas) { var wrappedLatLng = latLng.clone(); var mapCenterLng = this._map.getCenter().lng; var wrapAroundDiff = mapCenterLng - wrappedLatLng.lng; if (wrapAroundDiff < -180 || wrapAroundDiff > 180) { wrappedLatLng.lng += Math.round(wrapAroundDiff / 360) * 360; } return wrappedLatLng; } else { return latLng; } }; }