17 changed files with 898 additions and 44 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,416 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 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 { |
|||
DataLayerColorProcessor, |
|||
DataLayerPatternProcessor, |
|||
MapDataLayerType, TbDataLayerItem, |
|||
TbMapDataLayer, |
|||
UnplacedMapDataItem |
|||
} from '@home/components/widget/lib/maps/data-layer/map-data-layer'; |
|||
import { |
|||
BaseMarkerShapeSettings, ClusterMarkerColorFunction, |
|||
DataLayerColorType, defaultBaseMarkersDataLayerSettings, defaultBaseTripsDataLayerSettings, |
|||
loadImageWithAspect, |
|||
MapStringFunction, MapType, |
|||
MarkerIconInfo, |
|||
MarkerIconSettings, |
|||
MarkerImageFunction, |
|||
MarkerImageInfo, |
|||
MarkerImageSettings, |
|||
MarkerImageType, |
|||
MarkerPositionFunction, MarkersDataLayerSettings, |
|||
MarkerShapeSettings, |
|||
MarkerType, |
|||
TbMapDatasource, |
|||
TripsDataLayerSettings |
|||
} from '@home/components/widget/lib/maps/models/map.models'; |
|||
import { forkJoin, Observable, of } from 'rxjs'; |
|||
import { FormattedData } from '@shared/models/widget.models'; |
|||
import { |
|||
createColorMarkerIconElement, |
|||
createColorMarkerShapeURI, |
|||
MarkerShape |
|||
} from '@home/components/widget/lib/maps/models/marker-shape.models'; |
|||
import tinycolor from 'tinycolor2'; |
|||
import { MatIconRegistry } from '@angular/material/icon'; |
|||
import { DomSanitizer } from '@angular/platform-browser'; |
|||
import { catchError, map, switchMap } from 'rxjs/operators'; |
|||
import L from 'leaflet'; |
|||
import { CompiledTbFunction } from '@shared/models/js-function.models'; |
|||
import { isDefined, isDefinedAndNotNull, parseTbFunction, safeExecuteTbFunction } from '@core/utils'; |
|||
import { ImagePipe } from '@shared/pipe/image.pipe'; |
|||
import { TbMap } from '@home/components/widget/lib/maps/map'; |
|||
import { DataKeyType } from '@shared/models/telemetry/telemetry.models'; |
|||
|
|||
abstract class TripMarkerIconProcessor<S> { |
|||
|
|||
static fromSettings(dataLayer: TbTripsDataLayer, |
|||
settings: TripsDataLayerSettings): TripMarkerIconProcessor<any> { |
|||
switch (settings.markerType) { |
|||
case MarkerType.shape: |
|||
return new ShapeTripMarkerIconProcessor(dataLayer, settings.markerShape); |
|||
case MarkerType.icon: |
|||
return new IconTripMarkerIconProcessor(dataLayer, settings.markerIcon); |
|||
case MarkerType.image: |
|||
return new ImageTripMarkerIconProcessor(dataLayer, settings.markerImage); |
|||
} |
|||
} |
|||
|
|||
protected constructor(protected dataLayer: TbTripsDataLayer, |
|||
protected settings: S) {} |
|||
|
|||
public abstract setup(): Observable<void>; |
|||
|
|||
public abstract createMarkerIcon(data: FormattedData<TbMapDatasource>, |
|||
dsData: FormattedData<TbMapDatasource>[]): Observable<MarkerIconInfo>; |
|||
|
|||
} |
|||
|
|||
abstract class BaseColorTripMarkerShapeProcessor<S extends BaseMarkerShapeSettings> extends TripMarkerIconProcessor<S> { |
|||
|
|||
private markerColorFunction: CompiledTbFunction<MapStringFunction>; |
|||
|
|||
protected constructor(protected dataLayer: TbTripsDataLayer, |
|||
protected settings: S) { |
|||
super(dataLayer, settings); |
|||
} |
|||
|
|||
public setup(): Observable<void> { |
|||
const colorSettings = this.settings.color; |
|||
if (colorSettings.type === DataLayerColorType.function) { |
|||
return parseTbFunction<MapStringFunction>(this.dataLayer.getCtx().http, colorSettings.colorFunction, ['data', 'dsData']).pipe( |
|||
map((parsed) => { |
|||
this.markerColorFunction = parsed; |
|||
return null; |
|||
}) |
|||
); |
|||
} else { |
|||
return of(null); |
|||
} |
|||
} |
|||
|
|||
public createMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): Observable<MarkerIconInfo> { |
|||
const colorSettings = this.settings.color; |
|||
let color: tinycolor.Instance; |
|||
if (colorSettings.type === DataLayerColorType.function) { |
|||
const functionColor = safeExecuteTbFunction(this.markerColorFunction, [data, dsData]); |
|||
if (isDefinedAndNotNull(functionColor)) { |
|||
color = tinycolor(functionColor); |
|||
} else { |
|||
color = tinycolor(colorSettings.color); |
|||
} |
|||
} else { |
|||
color = tinycolor(colorSettings.color); |
|||
} |
|||
return this.createMarkerShape(color, data.rotationAngle, this.settings.size); |
|||
} |
|||
|
|||
protected abstract createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable<MarkerIconInfo>; |
|||
} |
|||
|
|||
class ShapeTripMarkerIconProcessor extends BaseColorTripMarkerShapeProcessor<MarkerShapeSettings> { |
|||
|
|||
constructor(protected dataLayer: TbTripsDataLayer, |
|||
protected settings: MarkerShapeSettings) { |
|||
super(dataLayer, settings); |
|||
} |
|||
|
|||
protected createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable<MarkerIconInfo> { |
|||
return this.dataLayer.createColoredMarkerShape(this.settings.shape, color, rotationAngle, size); |
|||
} |
|||
|
|||
} |
|||
|
|||
class IconTripMarkerIconProcessor extends BaseColorTripMarkerShapeProcessor<MarkerIconSettings> { |
|||
|
|||
constructor(protected dataLayer: TbTripsDataLayer, |
|||
protected settings: MarkerIconSettings) { |
|||
super(dataLayer, settings); |
|||
} |
|||
|
|||
protected createMarkerShape(color: tinycolor.Instance, rotationAngle: number, size: number): Observable<MarkerIconInfo> { |
|||
return this.dataLayer.createColoredMarkerIcon(this.settings.icon, color, rotationAngle, size); |
|||
} |
|||
|
|||
} |
|||
|
|||
class ImageTripMarkerIconProcessor extends TripMarkerIconProcessor<MarkerImageSettings> { |
|||
|
|||
private markerImageFunction: CompiledTbFunction<MarkerImageFunction>; |
|||
|
|||
constructor(protected dataLayer: TbTripsDataLayer, |
|||
protected settings: MarkerImageSettings) { |
|||
super(dataLayer, settings); |
|||
} |
|||
|
|||
public setup(): Observable<void> { |
|||
if (this.settings.type === MarkerImageType.function) { |
|||
return parseTbFunction<MarkerImageFunction>(this.dataLayer.getCtx().http, this.settings.imageFunction, ['data', 'images', 'dsData']).pipe( |
|||
map((parsed) => { |
|||
this.markerImageFunction = parsed; |
|||
return null; |
|||
}) |
|||
); |
|||
} else { |
|||
return of(null); |
|||
} |
|||
} |
|||
|
|||
public createMarkerIcon(data: FormattedData<TbMapDatasource>, dsData: FormattedData<TbMapDatasource>[]): Observable<MarkerIconInfo> { |
|||
if (this.settings.type === MarkerImageType.function) { |
|||
const currentImage: MarkerImageInfo = safeExecuteTbFunction(this.markerImageFunction, [data, this.settings.images, dsData]); |
|||
return this.loadMarkerIconInfo(currentImage, data.rotationAngle); |
|||
} else { |
|||
const currentImage: MarkerImageInfo = { |
|||
url: this.settings.image, |
|||
size: this.settings.imageSize || 34 |
|||
}; |
|||
return this.loadMarkerIconInfo(currentImage, data.rotationAngle); |
|||
} |
|||
} |
|||
|
|||
private loadMarkerIconInfo(image: MarkerImageInfo, rotationAngle = 0): Observable<MarkerIconInfo> { |
|||
if (image && image.url) { |
|||
return loadImageWithAspect(this.dataLayer.getCtx().$injector.get(ImagePipe), image.url).pipe( |
|||
switchMap((aspectImage) => { |
|||
if (aspectImage?.aspect) { |
|||
let width: number; |
|||
let height: number; |
|||
if (aspectImage.aspect > 1) { |
|||
width = image.size; |
|||
height = image.size / aspectImage.aspect; |
|||
} else { |
|||
width = image.size * aspectImage.aspect; |
|||
height = image.size; |
|||
} |
|||
let iconAnchor = image.markerOffset; |
|||
let popupAnchor = image.tooltipOffset; |
|||
if (!iconAnchor) { |
|||
iconAnchor = [width * this.dataLayer.markerOffset[0], height * this.dataLayer.markerOffset[1]]; |
|||
} |
|||
if (!popupAnchor) { |
|||
popupAnchor = [width * this.dataLayer.tooltipOffset[0], height * this.dataLayer.tooltipOffset[1]]; |
|||
} |
|||
const style = `background-image: url(${aspectImage.url}); transform: rotate(${rotationAngle}deg); height: ${height}px; width: ${width}px;`; |
|||
const icon = L.divIcon({ |
|||
html: `<div style="${style}"></div>`, |
|||
className: 'tb-marker-div-icon', |
|||
iconSize: [width, height], |
|||
iconAnchor, |
|||
popupAnchor |
|||
}); |
|||
const iconInfo: MarkerIconInfo = { |
|||
size: [width, height], |
|||
icon |
|||
}; |
|||
return of(iconInfo); |
|||
} else { |
|||
return this.dataLayer.createDefaultMarkerIcon(rotationAngle); |
|||
} |
|||
}), |
|||
catchError(() => this.dataLayer.createDefaultMarkerIcon(rotationAngle)) |
|||
); |
|||
} else { |
|||
return this.dataLayer.createDefaultMarkerIcon(rotationAngle); |
|||
} |
|||
} |
|||
} |
|||
|
|||
export class TbTripsDataLayer extends TbMapDataLayer<TripsDataLayerSettings, TbTripsDataLayer> { |
|||
|
|||
public tripMarkerIconProcessor: TripMarkerIconProcessor<any>; |
|||
|
|||
public markerOffset: L.LatLngTuple; |
|||
public tooltipOffset: L.LatLngTuple; |
|||
|
|||
public pathStrokeColorProcessor: DataLayerColorProcessor; |
|||
public pointColorProcessor: DataLayerColorProcessor; |
|||
public pointTooltipProcessor: DataLayerPatternProcessor; |
|||
|
|||
private positionFunction: CompiledTbFunction<MarkerPositionFunction>; |
|||
|
|||
private rawTripsData: FormattedData<TbMapDatasource>[][]; |
|||
private latestTripsData: FormattedData<TbMapDatasource>[]; |
|||
|
|||
constructor(protected map: TbMap<any>, |
|||
inputSettings: TripsDataLayerSettings) { |
|||
super(map, inputSettings); |
|||
} |
|||
|
|||
public dataLayerType(): MapDataLayerType { |
|||
return MapDataLayerType.trip; |
|||
} |
|||
|
|||
public placeItem(item: UnplacedMapDataItem, layer: L.Layer): void { |
|||
throw new Error('Not implemented!'); |
|||
} |
|||
|
|||
protected setupDatasource(datasource: TbMapDatasource): TbMapDatasource { |
|||
datasource.dataKeys = [this.settings.xKey, this.settings.yKey]; |
|||
if (this.settings.additionalDataKeys?.length) { |
|||
const tsKeys = this.settings.additionalDataKeys.filter(key => key.type === DataKeyType.timeseries); |
|||
const latestKeys = this.settings.additionalDataKeys.filter(key => key.type !== DataKeyType.timeseries); |
|||
datasource.dataKeys.push(...tsKeys); |
|||
if (latestKeys.length) { |
|||
datasource.latestDataKeys = latestKeys; |
|||
} |
|||
} |
|||
return datasource; |
|||
} |
|||
|
|||
protected defaultBaseSettings(map: TbMap<any>): Partial<TripsDataLayerSettings> { |
|||
return defaultBaseTripsDataLayerSettings(map.type()); |
|||
} |
|||
|
|||
protected doSetup(): Observable<void> { |
|||
this.markerOffset = [ |
|||
isDefined(this.settings.markerOffsetX) ? this.settings.markerOffsetX : 0.5, |
|||
isDefined(this.settings.markerOffsetY) ? this.settings.markerOffsetY : 1, |
|||
]; |
|||
this.tooltipOffset = [ |
|||
isDefined(this.settings.tooltip?.offsetX) ? this.settings.tooltip?.offsetX : 0, |
|||
isDefined(this.settings.tooltip?.offsetY) ? this.settings.tooltip?.offsetY : -1, |
|||
]; |
|||
this.tripMarkerIconProcessor = TripMarkerIconProcessor.fromSettings(this, this.settings); |
|||
const setup$: Observable<void>[] = []; |
|||
if (this.map.type() === MapType.image) { |
|||
setup$.push( |
|||
parseTbFunction<MarkerPositionFunction>(this.getCtx().http, this.settings.positionFunction, ['origXPos', 'origYPos', 'data', 'dsData', 'aspect']).pipe( |
|||
map((parsed) => { |
|||
this.positionFunction = parsed; |
|||
return null; |
|||
}) |
|||
) |
|||
); |
|||
} |
|||
setup$.push(this.tripMarkerIconProcessor.setup()); |
|||
|
|||
if (this.settings.showPath) { |
|||
this.pathStrokeColorProcessor = new DataLayerColorProcessor(this, this.settings.pathStrokeColor); |
|||
setup$.push(this.pathStrokeColorProcessor.setup()); |
|||
} |
|||
|
|||
if (this.settings.showPoints) { |
|||
this.pointColorProcessor = new DataLayerColorProcessor(this, this.settings.pointColor); |
|||
setup$.push(this.pointColorProcessor.setup()); |
|||
if (this.settings.pointTooltip?.show) { |
|||
this.pointTooltipProcessor = new DataLayerPatternProcessor(this, this.settings.pointTooltip); |
|||
setup$.push(this.pointTooltipProcessor.setup()); |
|||
} |
|||
} |
|||
return forkJoin(setup$).pipe(map(() => null)); |
|||
} |
|||
|
|||
protected isValidLayerData(_layerData: FormattedData<TbMapDatasource>): boolean { |
|||
throw new Error('Not implemented!'); |
|||
} |
|||
|
|||
protected createLayerItem(_data: FormattedData<TbMapDatasource>, _dsData: FormattedData<TbMapDatasource>[]): TbDataLayerItem<TripsDataLayerSettings,TbTripsDataLayer> { |
|||
throw new Error('Not implemented!'); |
|||
} |
|||
|
|||
public updateData(_dsData: FormattedData<TbMapDatasource>[]) { |
|||
throw new Error('Not implemented!'); |
|||
} |
|||
|
|||
public prepareTripsData(tripsData: FormattedData<TbMapDatasource>[][], tripsLatestData: FormattedData<TbMapDatasource>[]): {minTime: number; maxTime: number} { |
|||
let minTime = Infinity; |
|||
let maxTime = -Infinity; |
|||
this.rawTripsData = |
|||
tripsData.filter(d => d[0].$datasource.mapDataIds.includes(this.mapDataId)).map( |
|||
item => this.clearIncorrectFirsLastDatapoint(item)).filter(arr => arr.length); |
|||
this.latestTripsData = tripsLatestData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId)); |
|||
this.rawTripsData.forEach((dataSource) => { |
|||
minTime = Math.min(dataSource[0].time, minTime); |
|||
maxTime = Math.max(dataSource[dataSource.length - 1].time, maxTime); |
|||
}); |
|||
|
|||
return {minTime, maxTime}; |
|||
} |
|||
|
|||
public updateTrips(minTime: number, maxTime: number) { |
|||
console.log(`Update trips: min(${minTime}), max(${maxTime})`); |
|||
} |
|||
|
|||
public updateTripsLatestData(tripsLatestData: FormattedData<TbMapDatasource>[]) { |
|||
this.latestTripsData = tripsLatestData.filter(d => d.$datasource.mapDataIds.includes(this.mapDataId)); |
|||
} |
|||
|
|||
private clearIncorrectFirsLastDatapoint(dataSource: FormattedData<TbMapDatasource>[]): FormattedData<TbMapDatasource>[] { |
|||
const firstHistoricalDataIndexCoordinate = dataSource.findIndex(this.findFirstHistoricalDataIndexCoordinate); |
|||
if (firstHistoricalDataIndexCoordinate === -1) { |
|||
return []; |
|||
} |
|||
let lastIndex = dataSource.length - 1; |
|||
for (lastIndex; lastIndex > 0; lastIndex--) { |
|||
if (this.findFirstHistoricalDataIndexCoordinate(dataSource[lastIndex])) { |
|||
lastIndex++; |
|||
break; |
|||
} |
|||
} |
|||
if (firstHistoricalDataIndexCoordinate > 0 || lastIndex < dataSource.length) { |
|||
return dataSource.slice(firstHistoricalDataIndexCoordinate, lastIndex); |
|||
} |
|||
return dataSource; |
|||
} |
|||
|
|||
private findFirstHistoricalDataIndexCoordinate = (item: FormattedData<TbMapDatasource>): boolean => { |
|||
return isDefined(item[this.settings.xKey.label]) && isDefined(item[this.settings.yKey.label]); |
|||
} |
|||
|
|||
public createDefaultMarkerIcon(rotationAngle = 0): Observable<MarkerIconInfo> { |
|||
const color = this.settings.markerShape?.color?.color || '#307FE5'; |
|||
return this.createColoredMarkerShape(MarkerShape.markerShape1, tinycolor(color), rotationAngle); |
|||
} |
|||
|
|||
public createColoredMarkerShape(shape: MarkerShape, color: tinycolor.Instance, rotationAngle = 0, size = 34): Observable<MarkerIconInfo> { |
|||
return createColorMarkerShapeURI(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), shape, color).pipe( |
|||
map((iconUrl) => { |
|||
const style = `background-image: url(${iconUrl}); transform: rotate(${rotationAngle}deg); height: ${size}px; width: ${size}px;`; |
|||
return { |
|||
size: [size, size], |
|||
icon: L.divIcon({ |
|||
html: `<div style="${style}"></div>`, |
|||
className: 'tb-marker-div-icon', |
|||
iconSize: [size, size], |
|||
iconAnchor: [size * this.markerOffset[0], size * this.markerOffset[1]], |
|||
popupAnchor: [size * this.tooltipOffset[0], size * this.tooltipOffset[1]] |
|||
}) |
|||
}; |
|||
}) |
|||
); |
|||
} |
|||
|
|||
public createColoredMarkerIcon(icon: string, color: tinycolor.Instance, rotationAngle = 0, size = 34): Observable<MarkerIconInfo> { |
|||
return createColorMarkerIconElement(this.getCtx().$injector.get(MatIconRegistry), this.getCtx().$injector.get(DomSanitizer), icon, color).pipe( |
|||
map((element) => { |
|||
element.style.transform = `rotate(${rotationAngle}deg)`; |
|||
return { |
|||
size: [size, size], |
|||
icon: L.divIcon({ |
|||
html: element.outerHTML, |
|||
className: 'tb-marker-div-icon', |
|||
iconSize: [size, size], |
|||
iconAnchor: [size * this.markerOffset[0], size * this.markerOffset[1]], |
|||
popupAnchor: [size * this.tooltipOffset[0], size * this.tooltipOffset[1]] |
|||
}) |
|||
}; |
|||
}) |
|||
); |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
<!-- |
|||
|
|||
Copyright © 2016-2025 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. |
|||
|
|||
--> |
|||
<div class="tb-map-timeline-panel"> |
|||
<mat-slider |
|||
[disabled]="disabled" |
|||
[min]="min" |
|||
[max]="max" |
|||
[step]="settings.timeStep"> |
|||
<input matSliderThumb [(ngModel)]="currentTime" (valueChange)="onTimeChange()"> |
|||
</mat-slider> |
|||
</div> |
|||
@ -0,0 +1,19 @@ |
|||
/** |
|||
* Copyright © 2016-2025 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. |
|||
*/ |
|||
.tb-map-timeline-panel { |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
///
|
|||
/// Copyright © 2016-2025 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 { |
|||
Component, |
|||
ElementRef, |
|||
EventEmitter, |
|||
Input, |
|||
OnDestroy, |
|||
OnInit, |
|||
Output, |
|||
ViewEncapsulation |
|||
} from '@angular/core'; |
|||
import { TripTimelineSettings } from '@home/components/widget/lib/maps/models/map.models'; |
|||
|
|||
@Component({ |
|||
selector: 'tb-map-timeline-panel', |
|||
templateUrl: './map-timeline-panel.component.html', |
|||
styleUrls: ['./map-timeline-panel.component.scss'], |
|||
encapsulation: ViewEncapsulation.None |
|||
}) |
|||
export class MapTimelinePanelComponent implements OnInit, OnDestroy { |
|||
|
|||
@Input() |
|||
settings: TripTimelineSettings; |
|||
|
|||
@Input() |
|||
disabled = false; |
|||
|
|||
@Input() |
|||
min = 0; |
|||
|
|||
@Input() |
|||
max = 10000; |
|||
|
|||
@Output() |
|||
timeChanged = new EventEmitter<number>(); |
|||
|
|||
currentTime = 0; |
|||
|
|||
constructor(public element: ElementRef<HTMLElement>) { |
|||
} |
|||
|
|||
ngOnInit() { |
|||
} |
|||
|
|||
ngOnDestroy() { |
|||
} |
|||
|
|||
public onTimeChange() { |
|||
//this.updateValueText();
|
|||
this.timeChanged.next(this.currentTime); |
|||
} |
|||
|
|||
} |
|||
Loading…
Reference in new issue