277 lines
10 KiB
TypeScript
Raw Normal View History

///
/// 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 _ from 'lodash';
import tinycolor from 'tinycolor2';
import {
AfterViewInit,
ChangeDetectorRef,
Component,
Input,
OnDestroy,
OnInit,
SecurityContext,
ViewChild
} from '@angular/core';
import { MapWidgetController, TbMapWidgetV2 } from '../lib/maps/map-widget2';
2020-05-21 18:56:03 +03:00
import { FormattedData, MapProviders, TripAnimationSettings } from '../lib/maps/map-models';
2020-05-14 11:53:26 +03:00
import { addCondition, addGroupInfo, addToSchema, initSchema } from '@app/core/schema-utils';
import { mapPolygonSchema, pathSchema, pointSchema, tripAnimationSchema } from '../lib/maps/schemes';
import { DomSanitizer } from '@angular/platform-browser';
import { WidgetContext } from '@app/modules/home/models/widget-component.models';
2020-05-18 10:58:46 +03:00
import {
findAngle,
getRatio,
interpolateOnLineSegment,
parseArray,
parseFunction,
2020-05-18 10:58:46 +03:00
parseWithTranslation,
safeExecute
} from '../lib/maps/maps-utils';
2020-03-20 17:47:59 +02:00
import { JsonSettingsSchema, WidgetConfig } from '@shared/models/widget.models';
2020-04-24 11:22:26 +03:00
import moment from 'moment';
2020-05-18 10:58:46 +03:00
import { isUndefined } from '@core/utils';
import { ResizeObserver } from '@juggle/resize-observer';
interface dataMap {
[key: string] : FormattedData
}
@Component({
2020-03-20 17:47:59 +02:00
// tslint:disable-next-line:component-selector
selector: 'trip-animation',
templateUrl: './trip-animation.component.html',
styleUrls: ['./trip-animation.component.scss']
})
export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy {
private mapResize$: ResizeObserver;
constructor(private cd: ChangeDetectorRef, private sanitizer: DomSanitizer) { }
@Input() ctx: WidgetContext;
@ViewChild('map') mapContainer;
mapWidget: MapWidgetController;
2020-05-18 10:58:46 +03:00
historicalData: FormattedData[][];
normalizationStep: number;
interpolatedTimeData = [];
widgetConfig: WidgetConfig;
2020-05-21 18:56:03 +03:00
settings: TripAnimationSettings;
mainTooltips = [];
visibleTooltip = false;
2020-05-18 10:58:46 +03:00
activeTrip: FormattedData;
2020-05-27 11:08:06 +03:00
label: string;
2020-05-18 10:58:46 +03:00
minTime: number;
maxTime: number;
anchors: number[] = [];
2020-05-18 10:58:46 +03:00
useAnchors: boolean;
2020-05-19 14:59:25 +03:00
currentTime: number;
static getSettingsSchema(): JsonSettingsSchema {
const schema = initSchema();
2020-05-06 09:33:43 +03:00
addToSchema(schema, TbMapWidgetV2.getProvidersSchema(null, true));
addGroupInfo(schema, 'Map Provider Settings');
addToSchema(schema, tripAnimationSchema);
addGroupInfo(schema, 'Trip Animation Settings');
2020-05-06 09:33:43 +03:00
addToSchema(schema, pathSchema);
addGroupInfo(schema, 'Path Settings');
2020-05-06 19:09:41 +03:00
addToSchema(schema, addCondition(pointSchema, 'model.showPoints === true', ['showPoints']));
addGroupInfo(schema, 'Path Points Settings');
2020-05-06 09:33:43 +03:00
addToSchema(schema, addCondition(mapPolygonSchema, 'model.showPolygon === true', ['showPolygon']));
addGroupInfo(schema, 'Polygon Settings');
return schema;
}
ngOnInit(): void {
this.widgetConfig = this.ctx.widgetConfig;
const settings = {
normalizationStep: 1000,
showLabel: false,
buttonColor: tinycolor(this.widgetConfig.color).setAlpha(0.54).toRgbString(),
disabledButtonColor: tinycolor(this.widgetConfig.color).setAlpha(0.3).toRgbString(),
rotationAngle: 0
}
this.settings = { ...settings, ...this.ctx.settings };
2020-05-14 11:53:26 +03:00
this.useAnchors = this.settings.showPoints && this.settings.usePointAsAnchor;
this.settings.pointAsAnchorFunction = parseFunction(this.settings.pointAsAnchorFunction, ['data', 'dsData', 'dsIndex']);
2020-05-21 16:46:25 +03:00
this.settings.tooltipFunction = parseFunction(this.settings.tooltipFunction, ['data', 'dsData', 'dsIndex']);
this.settings.labelFunction = parseFunction(this.settings.labelFunction, ['data', 'dsData', 'dsIndex']);
2020-05-06 19:09:41 +03:00
this.normalizationStep = this.settings.normalizationStep;
2020-06-25 20:08:07 +03:00
const subscription = this.ctx.defaultSubscription;
subscription.callbacks.onDataUpdated = () => {
2020-05-19 11:02:36 +03:00
this.historicalData = parseArray(this.ctx.data).filter(arr => arr.length);
if (this.historicalData.length) {
this.calculateIntervals();
this.timeUpdated(this.minTime);
2020-05-19 11:02:36 +03:00
}
this.mapWidget.map.map?.invalidateSize();
this.cd.detectChanges();
}
}
ngAfterViewInit() {
2020-06-25 20:08:07 +03:00
this.mapWidget = new MapWidgetController(MapProviders.openstreet, false, this.ctx, this.mapContainer.nativeElement);
this.mapResize$ = new ResizeObserver(() => {
this.mapWidget.resize();
});
this.mapResize$.observe(this.mapContainer.nativeElement);
}
ngOnDestroy() {
if (this.mapResize$) {
this.mapResize$.disconnect();
}
}
timeUpdated(time: number) {
2020-05-19 14:59:25 +03:00
this.currentTime = time;
let currentPosition = this.interpolatedTimeData
2020-05-19 11:02:36 +03:00
.map(dataSource => dataSource[time])
for(let j = 0; j < this.interpolatedTimeData.length; j++) {
if (isUndefined(currentPosition[j])) {
const timePoints = Object.keys(this.interpolatedTimeData[j]).map(item => parseInt(item, 10));
for (let i = 1; i < timePoints.length; i++) {
if (timePoints[i - 1] < time && timePoints[i] > time) {
const beforePosition = this.interpolatedTimeData[j][timePoints[i - 1]];
const afterPosition = this.interpolatedTimeData[j][timePoints[i]];
const ratio = getRatio(timePoints[i - 1], timePoints[i], time);
currentPosition[j] = {
...beforePosition,
time,
...interpolateOnLineSegment(beforePosition, afterPosition, this.settings.latKeyName, this.settings.lngKeyName, ratio)
}
break;
2020-05-18 10:58:46 +03:00
}
}
}
}
for(let j = 0; j < this.interpolatedTimeData.length; j++) {
if (isUndefined(currentPosition[j])) {
currentPosition[j] = this.calculateLastPoints(this.interpolatedTimeData[j], time);
}
}
this.calcLabel(currentPosition);
this.calcTooltip(currentPosition, true);
2020-08-10 20:05:11 +03:00
if (this.mapWidget && this.mapWidget.map && this.mapWidget.map.map) {
const formattedInterpolatedTimeData = this.interpolatedTimeData.map(ds => _.values(ds));
this.mapWidget.map.updatePolylines(formattedInterpolatedTimeData, true);
2020-04-24 11:22:26 +03:00
if (this.settings.showPolygon) {
2020-05-18 10:58:46 +03:00
this.mapWidget.map.updatePolygons(this.interpolatedTimeData);
2020-04-24 11:22:26 +03:00
}
2020-05-06 19:09:41 +03:00
if (this.settings.showPoints) {
this.mapWidget.map.updatePoints(formattedInterpolatedTimeData, this.calcTooltip);
2020-05-06 09:33:43 +03:00
}
this.mapWidget.map.updateMarkers(currentPosition, true, (trip) => {
2020-05-19 14:59:25 +03:00
this.activeTrip = trip;
this.timeUpdated(this.currentTime)
});
}
}
setActiveTrip() {
}
private calculateLastPoints(dataSource: dataMap, time: number): FormattedData {
const timeArr = Object.keys(dataSource);
let index = timeArr.findIndex((dtime, index) => {
return Number(dtime) >= time;
});
if(index !== -1) {
if(Number(timeArr[index]) !== time && index !== 0) {
index--;
}
} else {
index = timeArr.length - 1;
}
return dataSource[timeArr[index]];
}
calculateIntervals() {
this.historicalData.forEach((dataSource, index) => {
2020-05-18 10:58:46 +03:00
this.minTime = dataSource[0]?.time || Infinity;
this.maxTime = dataSource[dataSource.length - 1]?.time || -Infinity;
2020-05-22 16:00:46 +03:00
this.interpolatedTimeData[index] = this.interpolateArray(dataSource);
});
2020-05-21 16:46:25 +03:00
if(!this.activeTrip){
this.activeTrip = this.interpolatedTimeData.map(dataSource => dataSource[this.minTime]).filter(ds => ds)[0];
}
if (this.useAnchors) {
const anchorDate = Object.entries(_.union(this.interpolatedTimeData)[0]);
this.anchors = anchorDate
.filter((data: [string, FormattedData]) => safeExecute(this.settings.pointAsAnchorFunction, [data[1], anchorDate, data[1].dsIndex]))
.map(data => parseInt(data[0], 10));
}
}
calcTooltip = (points?: FormattedData[], isMainTooltip: boolean = false): string => {
let tooltipText;
if(isMainTooltip) {
this.mainTooltips = []
}
for (let point of points) {
const data = point ? point : this.activeTrip;
const tooltipPattern: string = this.settings.useTooltipFunction ?
safeExecute(this.settings.tooltipFunction, [data, this.historicalData, point.dsIndex]) : this.settings.tooltipPattern;
tooltipText = parseWithTranslation.parseTemplate(tooltipPattern, data, true);
if(isMainTooltip) {
this.mainTooltips.push(this.sanitizer.sanitize(SecurityContext.HTML, tooltipText));
}
this.cd.detectChanges();
this.activeTrip = point;
}
2020-05-06 19:09:41 +03:00
return tooltipText;
2020-04-24 11:22:26 +03:00
}
calcLabel(formattedDataArr: FormattedData[]) {
this.label = '';
for (let formattedData of formattedDataArr) {
const data = formattedData;
const labelText: string = this.settings.useLabelFunction ?
safeExecute(this.settings.labelFunction, [data, this.historicalData, data.dsIndex]) : this.settings.label;
const label = (parseWithTranslation.parseTemplate(labelText, data, true));
this.label = this.label.length ? this.label + ',' + label : label;
}
}
2020-05-22 16:00:46 +03:00
interpolateArray(originData: FormattedData[]) {
const result = {};
2020-05-18 10:58:46 +03:00
const latKeyName = this.settings.latKeyName;
const lngKeyName = this.settings.lngKeyName;
for (const data of originData) {
const currentTime = data.time;
2020-05-18 10:58:46 +03:00
const normalizeTime = this.minTime + Math.ceil((currentTime - this.minTime) / this.normalizationStep) * this.normalizationStep;
result[normalizeTime] = {
...data,
2020-05-22 16:00:46 +03:00
minTime: this.minTime !== Infinity ? moment(this.minTime).format('YYYY-MM-DD HH:mm:ss') : '',
maxTime: this.maxTime !== -Infinity ? moment(this.maxTime).format('YYYY-MM-DD HH:mm:ss') : '',
rotationAngle: this.settings.rotationAngle
};
}
const timeStamp = Object.keys(result);
for (let i = 0; i < timeStamp.length - 1; i++) {
result[timeStamp[i]].rotationAngle += findAngle(result[timeStamp[i]], result[timeStamp[i + 1]], latKeyName, lngKeyName)
}
return result;
}
}
export let TbTripAnimationWidget = TripAnimationComponent;