2020-03-20 16:57:35 +02:00
|
|
|
///
|
2021-01-11 13:42:16 +02:00
|
|
|
/// Copyright © 2016-2021 The Thingsboard Authors
|
2020-03-20 16:57:35 +02:00
|
|
|
///
|
|
|
|
|
/// 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';
|
|
|
|
|
|
2020-05-28 22:07:43 +03:00
|
|
|
import {
|
|
|
|
|
AfterViewInit,
|
|
|
|
|
ChangeDetectorRef,
|
|
|
|
|
Component,
|
|
|
|
|
Input,
|
|
|
|
|
OnDestroy,
|
|
|
|
|
OnInit,
|
|
|
|
|
SecurityContext,
|
|
|
|
|
ViewChild
|
|
|
|
|
} from '@angular/core';
|
2020-12-28 16:06:36 +02:00
|
|
|
import { FormattedData, MapProviders, TripAnimationSettings } from '@home/components/widget/lib/maps/map-models';
|
2020-05-14 11:53:26 +03:00
|
|
|
import { addCondition, addGroupInfo, addToSchema, initSchema } from '@app/core/schema-utils';
|
2020-12-28 16:06:36 +02:00
|
|
|
import { mapPolygonSchema, pathSchema, pointSchema, tripAnimationSchema } from '@home/components/widget/lib/maps/schemes';
|
2021-10-21 15:43:15 +03:00
|
|
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
2020-03-20 16:57:35 +02:00
|
|
|
import { WidgetContext } from '@app/modules/home/models/widget-component.models';
|
2020-05-18 10:58:46 +03:00
|
|
|
import {
|
2020-12-28 16:06:36 +02:00
|
|
|
findAngle, getProviderSchema,
|
2020-05-18 10:58:46 +03:00
|
|
|
getRatio,
|
|
|
|
|
interpolateOnLineSegment,
|
|
|
|
|
parseArray,
|
2020-05-18 21:55:28 +03:00
|
|
|
parseFunction,
|
2020-05-18 10:58:46 +03:00
|
|
|
parseWithTranslation,
|
|
|
|
|
safeExecute
|
2020-12-28 16:06:36 +02:00
|
|
|
} from '@home/components/widget/lib/maps/common-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';
|
2020-05-28 22:07:43 +03:00
|
|
|
import { ResizeObserver } from '@juggle/resize-observer';
|
2020-12-28 16:06:36 +02:00
|
|
|
import { MapWidgetInterface } from '@home/components/widget/lib/maps/map-widget.interface';
|
2020-03-20 16:57:35 +02:00
|
|
|
|
2020-12-28 16:06:36 +02:00
|
|
|
interface DataMap {
|
|
|
|
|
[key: string]: FormattedData;
|
2020-11-11 13:24:10 +02:00
|
|
|
}
|
2020-03-20 16:57:35 +02:00
|
|
|
|
|
|
|
|
@Component({
|
2020-03-20 17:47:59 +02:00
|
|
|
// tslint:disable-next-line:component-selector
|
2020-03-20 16:57:35 +02:00
|
|
|
selector: 'trip-animation',
|
|
|
|
|
templateUrl: './trip-animation.component.html',
|
|
|
|
|
styleUrls: ['./trip-animation.component.scss']
|
|
|
|
|
})
|
2020-05-28 22:07:43 +03:00
|
|
|
export class TripAnimationComponent implements OnInit, AfterViewInit, OnDestroy {
|
|
|
|
|
|
|
|
|
|
private mapResize$: ResizeObserver;
|
2020-03-20 16:57:35 +02:00
|
|
|
|
|
|
|
|
constructor(private cd: ChangeDetectorRef, private sanitizer: DomSanitizer) { }
|
|
|
|
|
|
|
|
|
|
@Input() ctx: WidgetContext;
|
|
|
|
|
|
|
|
|
|
@ViewChild('map') mapContainer;
|
|
|
|
|
|
2020-12-28 16:06:36 +02:00
|
|
|
mapWidget: MapWidgetInterface;
|
2020-05-18 10:58:46 +03:00
|
|
|
historicalData: FormattedData[][];
|
|
|
|
|
normalizationStep: number;
|
2021-10-21 15:43:15 +03:00
|
|
|
interpolatedTimeData: {[time: number]: FormattedData}[] = [];
|
|
|
|
|
formattedInterpolatedTimeData: FormattedData[][] = [];
|
2020-03-20 16:57:35 +02:00
|
|
|
widgetConfig: WidgetConfig;
|
2020-05-21 18:56:03 +03:00
|
|
|
settings: TripAnimationSettings;
|
2020-11-11 13:24:10 +02:00
|
|
|
mainTooltips = [];
|
2020-03-20 16:57:35 +02:00
|
|
|
visibleTooltip = false;
|
2020-05-18 10:58:46 +03:00
|
|
|
activeTrip: FormattedData;
|
2021-10-21 15:43:15 +03:00
|
|
|
label: SafeHtml;
|
2020-05-18 10:58:46 +03:00
|
|
|
minTime: number;
|
|
|
|
|
maxTime: number;
|
2020-05-18 21:55:28 +03:00
|
|
|
anchors: number[] = [];
|
2020-05-18 10:58:46 +03:00
|
|
|
useAnchors: boolean;
|
2020-05-19 14:59:25 +03:00
|
|
|
currentTime: number;
|
2020-03-20 16:57:35 +02:00
|
|
|
|
|
|
|
|
static getSettingsSchema(): JsonSettingsSchema {
|
|
|
|
|
const schema = initSchema();
|
2020-12-28 16:06:36 +02:00
|
|
|
addToSchema(schema, getProviderSchema(null, true));
|
2020-03-20 16:57:35 +02:00
|
|
|
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');
|
2020-03-20 16:57:35 +02:00
|
|
|
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
|
2020-12-28 16:06:36 +02:00
|
|
|
};
|
2020-03-20 16:57:35 +02:00
|
|
|
this.settings = { ...settings, ...this.ctx.settings };
|
2020-05-14 11:53:26 +03:00
|
|
|
this.useAnchors = this.settings.showPoints && this.settings.usePointAsAnchor;
|
2020-05-18 21:55:28 +03:00
|
|
|
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']);
|
2021-01-29 14:02:48 +02:00
|
|
|
this.settings.colorPointFunction = parseFunction(this.settings.colorPointFunction, ['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);
|
2021-10-21 15:43:15 +03:00
|
|
|
this.interpolatedTimeData.length = 0;
|
|
|
|
|
this.formattedInterpolatedTimeData.length = 0;
|
2020-05-19 11:02:36 +03:00
|
|
|
if (this.historicalData.length) {
|
|
|
|
|
this.calculateIntervals();
|
2020-11-11 13:24:10 +02:00
|
|
|
this.timeUpdated(this.minTime);
|
2020-05-19 11:02:36 +03:00
|
|
|
}
|
2020-03-20 16:57:35 +02:00
|
|
|
this.mapWidget.map.map?.invalidateSize();
|
|
|
|
|
this.cd.detectChanges();
|
2020-12-28 16:06:36 +02:00
|
|
|
};
|
2020-03-20 16:57:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ngAfterViewInit() {
|
2020-12-28 16:06:36 +02:00
|
|
|
import('@home/components/widget/lib/maps/map-widget2').then(
|
|
|
|
|
(mod) => {
|
|
|
|
|
this.mapWidget = new mod.MapWidgetController(MapProviders.openstreet, false, this.ctx, this.mapContainer.nativeElement);
|
|
|
|
|
this.mapResize$ = new ResizeObserver(() => {
|
|
|
|
|
this.mapWidget.resize();
|
|
|
|
|
});
|
|
|
|
|
this.mapResize$.observe(this.mapContainer.nativeElement);
|
|
|
|
|
}
|
|
|
|
|
);
|
2020-05-28 22:07:43 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ngOnDestroy() {
|
|
|
|
|
if (this.mapResize$) {
|
|
|
|
|
this.mapResize$.disconnect();
|
|
|
|
|
}
|
2020-03-20 16:57:35 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
timeUpdated(time: number) {
|
2020-05-19 14:59:25 +03:00
|
|
|
this.currentTime = time;
|
2021-10-21 15:43:15 +03:00
|
|
|
// get point for each datasource associated with time
|
2020-12-16 17:36:08 +02:00
|
|
|
const currentPosition = this.interpolatedTimeData
|
2020-12-28 16:06:36 +02:00
|
|
|
.map(dataSource => dataSource[time]);
|
|
|
|
|
for (let j = 0; j < this.interpolatedTimeData.length; j++) {
|
2020-11-11 13:24:10 +02:00
|
|
|
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)
|
2020-12-28 16:06:36 +02:00
|
|
|
};
|
2020-11-11 13:24:10 +02:00
|
|
|
break;
|
2020-05-18 10:58:46 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-28 16:06:36 +02:00
|
|
|
for (let j = 0; j < this.interpolatedTimeData.length; j++) {
|
2020-11-11 13:24:10 +02:00
|
|
|
if (isUndefined(currentPosition[j])) {
|
|
|
|
|
currentPosition[j] = this.calculateLastPoints(this.interpolatedTimeData[j], time);
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-10-21 15:43:15 +03:00
|
|
|
this.calcLabel(currentPosition);
|
2020-12-23 17:14:19 +02:00
|
|
|
this.calcMainTooltip(currentPosition);
|
2020-08-10 20:05:11 +03:00
|
|
|
if (this.mapWidget && this.mapWidget.map && this.mapWidget.map.map) {
|
2021-10-21 15:43:15 +03:00
|
|
|
this.mapWidget.map.updatePolylines(this.formattedInterpolatedTimeData, currentPosition, true);
|
2020-04-24 11:22:26 +03:00
|
|
|
if (this.settings.showPolygon) {
|
2021-10-21 15:43:15 +03:00
|
|
|
this.mapWidget.map.updatePolygons(currentPosition);
|
2020-04-24 11:22:26 +03:00
|
|
|
}
|
2021-01-29 14:02:48 +02:00
|
|
|
if (this.settings.showPoints) {
|
2021-10-21 15:43:15 +03:00
|
|
|
this.mapWidget.map.updatePoints(this.formattedInterpolatedTimeData, this.calcTooltip);
|
2020-05-06 09:33:43 +03:00
|
|
|
}
|
2020-06-24 20:25:50 +03:00
|
|
|
this.mapWidget.map.updateMarkers(currentPosition, true, (trip) => {
|
2020-05-19 14:59:25 +03:00
|
|
|
this.activeTrip = trip;
|
2020-12-28 16:06:36 +02:00
|
|
|
this.timeUpdated(this.currentTime);
|
2021-10-21 15:43:15 +03:00
|
|
|
this.cd.markForCheck();
|
2020-05-19 14:59:25 +03:00
|
|
|
});
|
2020-03-20 16:57:35 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setActiveTrip() {
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-28 16:06:36 +02:00
|
|
|
private calculateLastPoints(dataSource: DataMap, time: number): FormattedData {
|
2020-11-11 13:24:10 +02:00
|
|
|
const timeArr = Object.keys(dataSource);
|
2020-12-28 16:06:36 +02:00
|
|
|
let index = timeArr.findIndex((dtime) => {
|
2020-11-11 13:24:10 +02:00
|
|
|
return Number(dtime) >= time;
|
|
|
|
|
});
|
|
|
|
|
|
2020-12-28 16:06:36 +02:00
|
|
|
if (index !== -1) {
|
|
|
|
|
if (Number(timeArr[index]) !== time && index !== 0) {
|
2020-11-11 13:24:10 +02:00
|
|
|
index--;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
index = timeArr.length - 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dataSource[timeArr[index]];
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-20 16:57:35 +02:00
|
|
|
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-03-20 16:57:35 +02:00
|
|
|
});
|
2021-10-21 15:43:15 +03:00
|
|
|
this.formattedInterpolatedTimeData = this.interpolatedTimeData.map(ds => _.values(ds));
|
2020-12-28 16:06:36 +02:00
|
|
|
if (!this.activeTrip) {
|
2020-05-21 16:46:25 +03:00
|
|
|
this.activeTrip = this.interpolatedTimeData.map(dataSource => dataSource[this.minTime]).filter(ds => ds)[0];
|
|
|
|
|
}
|
2020-05-18 21:55:28 +03:00
|
|
|
if (this.useAnchors) {
|
|
|
|
|
const anchorDate = Object.entries(_.union(this.interpolatedTimeData)[0]);
|
|
|
|
|
this.anchors = anchorDate
|
2021-10-21 15:43:15 +03:00
|
|
|
.filter((data: [string, FormattedData], tsIndex) => safeExecute(this.settings.pointAsAnchorFunction, [data[1],
|
|
|
|
|
this.formattedInterpolatedTimeData.map(ds => ds[tsIndex]), data[1].dsIndex]))
|
2020-05-18 21:55:28 +03:00
|
|
|
.map(data => parseInt(data[0], 10));
|
|
|
|
|
}
|
2020-03-20 16:57:35 +02:00
|
|
|
}
|
|
|
|
|
|
2021-10-21 15:43:15 +03:00
|
|
|
calcTooltip = (point: FormattedData, points: FormattedData[]): string => {
|
2020-12-23 17:14:19 +02:00
|
|
|
const data = point ? point : this.activeTrip;
|
|
|
|
|
const tooltipPattern: string = this.settings.useTooltipFunction ?
|
2021-10-21 15:43:15 +03:00
|
|
|
safeExecute(this.settings.tooltipFunction,
|
|
|
|
|
[data, points, point.dsIndex]) : this.settings.tooltipPattern;
|
2020-12-23 17:14:19 +02:00
|
|
|
return parseWithTranslation.parseTemplate(tooltipPattern, data, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private calcMainTooltip(points: FormattedData[]): void {
|
|
|
|
|
const tooltips = [];
|
2020-12-28 16:06:36 +02:00
|
|
|
for (const point of points) {
|
2021-10-21 15:43:15 +03:00
|
|
|
tooltips.push(this.sanitizer.sanitize(SecurityContext.HTML, this.calcTooltip(point, points)));
|
2020-11-11 13:24:10 +02:00
|
|
|
}
|
2020-12-23 17:14:19 +02:00
|
|
|
this.mainTooltips = tooltips;
|
2020-04-24 11:22:26 +03:00
|
|
|
}
|
2020-03-20 16:57:35 +02:00
|
|
|
|
2021-10-21 15:43:15 +03:00
|
|
|
calcLabel(points: FormattedData[]) {
|
|
|
|
|
const data = points[this.activeTrip.dsIndex];
|
2020-12-23 17:14:19 +02:00
|
|
|
const labelText: string = this.settings.useLabelFunction ?
|
2021-10-21 15:43:15 +03:00
|
|
|
safeExecute(this.settings.labelFunction, [data, points, data.dsIndex]) : this.settings.label;
|
|
|
|
|
this.label = this.sanitizer.bypassSecurityTrustHtml(parseWithTranslation.parseTemplate(labelText, data, true));
|
2020-03-20 16:57:35 +02:00
|
|
|
}
|
|
|
|
|
|
2021-10-21 15:43:15 +03:00
|
|
|
interpolateArray(originData: FormattedData[]): {[time: number]: FormattedData} {
|
|
|
|
|
const result: {[time: number]: FormattedData} = {};
|
2020-05-18 10:58:46 +03:00
|
|
|
const latKeyName = this.settings.latKeyName;
|
|
|
|
|
const lngKeyName = this.settings.lngKeyName;
|
2020-05-18 21:55:28 +03:00
|
|
|
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;
|
2020-05-18 21:55:28 +03:00
|
|
|
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') : '',
|
2020-05-18 21:55:28 +03:00
|
|
|
rotationAngle: this.settings.rotationAngle
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
const timeStamp = Object.keys(result);
|
2020-05-19 11:43:00 +03:00
|
|
|
for (let i = 0; i < timeStamp.length - 1; i++) {
|
2020-12-28 16:06:36 +02:00
|
|
|
result[timeStamp[i]].rotationAngle += findAngle(result[timeStamp[i]], result[timeStamp[i + 1]], latKeyName, lngKeyName);
|
2020-03-20 16:57:35 +02:00
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export let TbTripAnimationWidget = TripAnimationComponent;
|