From 416d0458933548db86aed4cb5dcd2efbea13d54f Mon Sep 17 00:00:00 2001 From: ArtemHalushko <61501795+ArtemHalushko@users.noreply.github.com> Date: Thu, 7 May 2020 12:25:43 +0300 Subject: [PATCH] Map/3.0 (#2738) * WIP on trip-animation settings * trip-animation points & anchors * fixes Co-authored-by: Adsumus Co-authored-by: Igor Kulikov --- .../components/widget/lib/maps/leaflet-map.ts | 79 ++- .../components/widget/lib/maps/map-models.ts | 104 +++- .../widget/lib/maps/map-widget.interface.ts | 2 +- .../components/widget/lib/maps/map-widget2.ts | 100 +--- .../components/widget/lib/maps/markers.ts | 1 - .../components/widget/lib/maps/polygon.ts | 11 +- .../widget/lib/maps/providers/image-map.ts | 4 +- .../components/widget/lib/maps/schemes.ts | 455 ++++++++---------- .../trip-animation.component.html | 2 +- .../trip-animation.component.ts | 51 +- .../history-selector.component.ts | 26 +- 11 files changed, 439 insertions(+), 396 deletions(-) diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts index f4b264cabc..982c29105d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -14,18 +14,18 @@ /// limitations under the License. /// -import L, { LatLngBounds, LatLngTuple, markerClusterGroup, MarkerClusterGroupOptions } from 'leaflet'; +import L, { LatLngBounds, LatLngTuple, markerClusterGroup, MarkerClusterGroupOptions, FeatureGroup, LayerGroup } from 'leaflet'; import 'leaflet-providers'; import 'leaflet.markercluster/dist/leaflet.markercluster'; import { - FormattedData, - MapSettings, - MarkerSettings, - PolygonSettings, - PolylineSettings, - UnitedMapSettings + FormattedData, + MapSettings, + MarkerSettings, + PolygonSettings, + PolylineSettings, + UnitedMapSettings } from './map-models'; import { Marker } from './markers'; import { BehaviorSubject, Observable } from 'rxjs'; @@ -33,7 +33,7 @@ import { filter } from 'rxjs/operators'; import { Polyline } from './polyline'; import { Polygon } from './polygon'; import { DatasourceData } from '@app/shared/models/widget.models'; -import { safeExecute } from '@home/components/widget/lib/maps/maps-utils'; +import { safeExecute, createTooltip } from '@home/components/widget/lib/maps/maps-utils'; export default abstract class LeafletMap { @@ -47,6 +47,8 @@ export default abstract class LeafletMap { bounds: L.LatLngBounds; datasources: FormattedData[]; markersCluster; + points: FeatureGroup; + markersData = []; protected constructor(public $container: HTMLElement, options: UnitedMapSettings) { this.options = options; @@ -157,9 +159,9 @@ export default abstract class LeafletMap { this.map = map; if (this.options.useDefaultCenterPosition) { this.map.panTo(this.options.defaultCenterPosition); - this.bounds = map.getBounds(); + this.bounds = map.getBounds(); } - else this.bounds = new L.LatLngBounds(null, null); + else this.bounds = new L.LatLngBounds(null, null); if (this.options.draggableMarker) { this.addMarkerControl(); } @@ -200,9 +202,9 @@ export default abstract class LeafletMap { return this.map.getCenter(); } - fitBounds(bounds: LatLngBounds, useDefaultZoom = false, padding?: LatLngTuple) { + fitBounds(bounds: LatLngBounds, padding?: LatLngTuple) { if (bounds.isValid()) { - this.bounds = this.bounds.extend(bounds); + this.bounds = !!this.bounds ? this.bounds.extend(bounds) : bounds; if (!this.options.fitMapBounds && this.options.defaultZoomLevel) { this.map.setZoom(this.options.defaultZoomLevel, { animate: false }); if (this.options.useDefaultCenterPosition) { @@ -218,9 +220,9 @@ export default abstract class LeafletMap { } }); if (this.options.useDefaultCenterPosition) { - bounds = bounds.extend(this.options.defaultCenterPosition); + this.bounds = this.bounds.extend(this.options.defaultCenterPosition); } - this.map.fitBounds(bounds, { padding: padding || [50, 50], animate: false }); + this.map.fitBounds(this.bounds, { padding: padding || [50, 50], animate: false }); } } } @@ -252,11 +254,10 @@ export default abstract class LeafletMap { const style = currentImage ? 'background-image: url(' + currentImage.url + ');' : ''; this.options.icon = L.divIcon({ html: `
` - }) + style="transform: translate(-10px, -10px) + rotate(${data.rotationAngle}deg); + ${style}">
` + }); } else { this.options.icon = null; @@ -268,6 +269,7 @@ export default abstract class LeafletMap { this.createMarker(data.entityName, data, markersData, this.options as MarkerSettings); } }); + this.markersData = markersData; } dragMarker = (e, data?) => { @@ -278,7 +280,8 @@ export default abstract class LeafletMap { private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings) { this.ready$.subscribe(() => { const newMarker = new Marker(this.convertPosition(data), settings, data, dataSources, this.dragMarker); - this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng()), settings.draggableMarker && this.markers.size < 2); + if (this.bounds) + this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng())); this.markers.set(key, newMarker); if (this.options.useClusterMarkers) { this.markersCluster.addLayer(newMarker.leafletMarker); @@ -313,6 +316,29 @@ export default abstract class LeafletMap { } } + updatePoints(pointsData: FormattedData[], getTooltip: (point: FormattedData, setTooltip?: boolean) => string) { + this.map$.subscribe(map => { + if (this.points) { + map.removeLayer(this.points); + } + this.points = new FeatureGroup(); + pointsData.filter(pdata => !!this.convertPosition(pdata)).forEach(data => { + const point = L.circleMarker(this.convertPosition(data), { + color: this.options.pointColor, + radius: this.options.pointSize + }); + if (!this.options.pointTooltipOnRightPanel) { + point.on('click', () => getTooltip(data)); + } + else { + createTooltip(point, this.options, pointsData, getTooltip(data, false)); + } + this.points.addLayer(point); + }); + map.addLayer(this.points); + }); + } + setImageAlias(alias: Observable) { } @@ -337,15 +363,17 @@ export default abstract class LeafletMap { this.ready$.subscribe(() => { const poly = new Polyline(this.map, data.map(el => this.convertPosition(el)).filter(el => !!el), data, dataSources, settings); - const bounds = this.bounds.extend(poly.leafletPoly.getBounds()); - this.fitBounds(bounds) - this.polylines.set(data[0].entityName, poly) + const bounds = poly.leafletPoly.getBounds(); + this.fitBounds(bounds); + this.polylines.set(data[0].entityName, poly); }); } updatePolyline(key: string, data: FormattedData[], dataSources: FormattedData[], settings: PolylineSettings) { this.ready$.subscribe(() => { - this.polylines.get(key).updatePolyline(settings, data.map(el => this.convertPosition(el)), dataSources); + const poly = this.polylines.get(key); + poly.updatePolyline(settings, data.map(el => this.convertPosition(el)), dataSources); + const bounds = poly.leafletPoly.getBounds(); }); } @@ -370,7 +398,7 @@ export default abstract class LeafletMap { createPolygon(polyData: DatasourceData, dataSources: DatasourceData[], settings: PolygonSettings) { this.ready$.subscribe(() => { const polygon = new Polygon(this.map, polyData, dataSources, settings); - const bounds = this.bounds.extend(polygon.leafletPoly.getBounds()); + const bounds = polygon.leafletPoly.getBounds(); this.fitBounds(bounds); this.polygons.set(polyData.datasource.entityName, polygon); }); @@ -380,7 +408,6 @@ export default abstract class LeafletMap { this.ready$.subscribe(() => { const poly = this.polygons.get(polyData.datasource.entityName); poly.updatePolygon(polyData.data, dataSources, settings); - this.fitBounds(poly.leafletPoly.getBounds()); }); } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts index 16e1c809ce..28e9686bc1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts @@ -15,13 +15,19 @@ /// import { LatLngTuple } from 'leaflet'; -import { Datasource } from '@app/shared/models/widget.models'; +import { Datasource, JsonSettingsSchema } from '@app/shared/models/widget.models'; +import { Type } from '@angular/core'; +import LeafletMap from './leaflet-map'; +import { OpenStreetMap, TencentMap, GoogleMap, HEREMap, ImageMap } from './providers'; +import { + openstreetMapSettingsSchema, tencentMapSettingsSchema, + googleMapSettingsSchema, hereMapSettingsSchema, imageMapSettingsSchema +} from './schemes'; export type GenericFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string; export type MarkerImageFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string; export type MapSettings = { - polygonKeyName: any; draggableMarker: boolean; initCallback?: () => any; posFunction: (origXPos, origYPos) => { x, y }; @@ -108,7 +114,8 @@ export interface FormattedData { export type PolygonSettings = { showPolygon: boolean; - showTooltip: any; + polygonKeyName: string; + polKeyName: string;// deprecated polygonStrokeOpacity: number; polygonOpacity: number; polygonStrokeWeight: number; @@ -116,12 +123,13 @@ export type PolygonSettings = { polygonColor: string; showPolygonTooltip: boolean; autocloseTooltip: boolean; - tooltipFunction: GenericFunction; showTooltipAction: string; tooltipAction: { [name: string]: actionsHandler }; - tooltipPattern: string; - useTooltipFunction: boolean; + polygonTooltipPattern: string; + usePolygonTooltipFunction: boolean; polygonClick: { [name: string]: actionsHandler }; + usePolygonColorFunction: boolean; + polygonTooltipFunction: GenericFunction; polygonColorFunction?: GenericFunction; } @@ -154,6 +162,88 @@ export interface HistorySelectSettings { buttonColor: string; } +export type TripAnimationSttings = { + pointColor: string; + pointSize: number; + pointTooltipOnRightPanel: boolean; +} + export type actionsHandler = ($event: Event, datasource: Datasource) => void; -export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings; +export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings & TripAnimationSttings; + +interface IProvider { + MapClass: Type, + schema: JsonSettingsSchema, + name: string +} + +export const providerSets: { [key: string]: IProvider } = { + 'openstreet-map': { + MapClass: OpenStreetMap, + schema: openstreetMapSettingsSchema, + name: 'openstreet-map', + }, + 'tencent-map': { + MapClass: TencentMap, + schema: tencentMapSettingsSchema, + name: 'tencent-map' + }, + 'google-map': { + MapClass: GoogleMap, + schema: googleMapSettingsSchema, + name: 'google-map' + }, + here: { + MapClass: HEREMap, + schema: hereMapSettingsSchema, + name: 'here' + }, + 'image-map': { + MapClass: ImageMap, + schema: imageMapSettingsSchema, + name: 'image-map' + } +}; + +export const defaultSettings: any = { + xPosKeyName: 'xPos', + yPosKeyName: 'yPos', + markerOffsetX: 0.5, + markerOffsetY: 1, + latKeyName: 'latitude', + lngKeyName: 'longitude', + polygonKeyName: 'coordinates', + showLabel: false, + label: '${entityName}', + showTooltip: false, + useDefaultCenterPosition: false, + showTooltipAction: 'click', + autocloseTooltip: false, + showPolygon: false, + labelColor: '#000000', + color: '#FE7569', + polygonColor: '#0000ff', + polygonStrokeColor: '#fe0001', + polygonOpacity: 0.5, + polygonStrokeOpacity: 1, + polygonStrokeWeight: 1, + useLabelFunction: false, + markerImages: [], + strokeWeight: 2, + strokeOpacity: 1.0, + initCallback: () => { }, + defaultZoomLevel: 8, + disableScrollZooming: false, + minZoomLevel: 16, + credentials: '', + markerClusteringSetting: null, + draggableMarker: false, + fitMapBounds: true +}; + +export const hereProviders = [ + 'HERE.normalDay', + 'HERE.normalNight', + 'HERE.hybridDay', + 'HERE.terrainDay'] diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts index 9900d98e0b..be618128dd 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts @@ -26,7 +26,7 @@ export interface MapWidgetInterface { export interface MapWidgetStaticInterface { settingsSchema(mapProvider?: MapProviders, drawRoutes?: boolean): JsonSettingsSchema; - getProvidersSchema(mapProvider?: MapProviders): JsonSettingsSchema + getProvidersSchema(mapProvider?: MapProviders, ignoreImageMap?: boolean): JsonSettingsSchema dataKeySettingsSchema(): object; actionSources(): object; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts index 5c14ac80c3..fb3e6662f3 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts @@ -14,23 +14,17 @@ /// limitations under the License. /// -import { MapProviders, UnitedMapSettings } from './map-models'; +import { MapProviders, UnitedMapSettings, providerSets, hereProviders, defaultSettings } from './map-models'; import LeafletMap from './leaflet-map'; import { - openstreetMapSettingsSchema, - googleMapSettingsSchema, - imageMapSettingsSchema, - tencentMapSettingsSchema, commonMapSettingsSchema, routeMapSettingsSchema, markerClusteringSettingsSchema, markerClusteringSettingsSchemaLeaflet, - hereMapSettingsSchema, mapProviderSchema, mapPolygonSchema } from './schemes'; import { MapWidgetStaticInterface, MapWidgetInterface } from './map-widget.interface'; -import { OpenStreetMap, TencentMap, GoogleMap, HEREMap, ImageMap } from './providers'; import { initSchema, addToSchema, mergeSchemes, addCondition, addGroupInfo } from '@core/schema-utils'; import { of, Subject } from 'rxjs'; import { WidgetContext } from '@app/modules/home/models/widget-component.models'; @@ -39,7 +33,6 @@ import { JsonSettingsSchema, WidgetActionDescriptor, DatasourceType, widgetType, import { EntityId } from '@shared/models/id/entity-id'; import { AttributeScope, DataKeyType, LatestTelemetry } from '@shared/models/telemetry/telemetry.models'; import { AttributeService } from '@core/http/attribute.service'; -import { Type } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { UtilsService } from '@core/services/utils.service'; @@ -85,11 +78,19 @@ export class MapWidgetController implements MapWidgetInterface { return {}; } - public static getProvidersSchema(mapProvider: MapProviders) { - mapProviderSchema.schema.properties.provider.default = mapProvider; - return mergeSchemes([mapProviderSchema, + public static getProvidersSchema(mapProvider: MapProviders, ignoreImageMap = false) { + if (mapProvider) + mapProviderSchema.schema.properties.provider.default = mapProvider; + const providerSchema = mapProviderSchema; + if (ignoreImageMap) { + providerSchema.form[0].items = providerSchema.form[0]?.items.filter(item => item.value !== 'image-map'); + } + return mergeSchemes([providerSchema, ...Object.keys(providerSets)?.map( - (key: string) => { const setting = providerSets[key]; return addCondition(setting?.schema, `model.provider === '${setting.name}'`) })]); + (key: string) => { + const setting = providerSets[key]; + return addCondition(setting?.schema, `model.provider === '${setting.name}'`); + })]); } public static settingsSchema(mapProvider: MapProviders, drawRoutes: boolean): JsonSettingsSchema { @@ -218,6 +219,7 @@ export class MapWidgetController implements MapWidgetInterface { polygonColorFunction: parseFunction(settings.polygonColorFunction, functionParams), markerImageFunction: parseFunction(settings.markerImageFunction, ['data', 'images', 'dsData', 'dsIndex']), labelColor: this.ctx.widgetConfig.color, + polygonKeyName: settings.polKeyName ? settings.polKeyName : settings.polygonKeyName, tooltipPattern: settings.tooltipPattern || '${entityName}

Latitude: ${' + settings.latKeyName + ':7}
Longitude: ${' + settings.lngKeyName + ':7}', @@ -295,78 +297,4 @@ export class MapWidgetController implements MapWidgetInterface { export let TbMapWidgetV2: MapWidgetStaticInterface = MapWidgetController; -interface IProvider { - MapClass: Type, - schema: JsonSettingsSchema, - name: string -} - -export const providerSets: { [key: string]: IProvider } = { - 'openstreet-map': { - MapClass: OpenStreetMap, - schema: openstreetMapSettingsSchema, - name: 'openstreet-map', - }, - 'tencent-map': { - MapClass: TencentMap, - schema: tencentMapSettingsSchema, - name: 'tencent-map' - }, - 'google-map': { - MapClass: GoogleMap, - schema: googleMapSettingsSchema, - name: 'google-map' - }, - here: { - MapClass: HEREMap, - schema: hereMapSettingsSchema, - name: 'here' - }, - 'image-map': { - MapClass: ImageMap, - schema: imageMapSettingsSchema, - name: 'image-map' - } -}; - -export const defaultSettings: any = { - xPosKeyName: 'xPos', - yPosKeyName: 'yPos', - markerOffsetX: 0.5, - markerOffsetY: 1, - latKeyName: 'latitude', - lngKeyName: 'longitude', - polygonKeyName: 'coordinates', - showLabel: false, - label: '${entityName}', - showTooltip: false, - useDefaultCenterPosition: false, - showTooltipAction: 'click', - autocloseTooltip: false, - showPolygon: false, - labelColor: '#000000', - color: '#FE7569', - polygonColor: '#0000ff', - polygonStrokeColor: '#fe0001', - polygonOpacity: 0.5, - polygonStrokeOpacity: 1, - polygonStrokeWeight: 1, - useLabelFunction: false, - markerImages: [], - strokeWeight: 2, - strokeOpacity: 1.0, - initCallback: () => { }, - defaultZoomLevel: 8, - disableScrollZooming: false, - minZoomLevel: 16, - credentials: '', - markerClusteringSetting: null, - draggableMarker: false, - fitMapBounds: true -}; -export const hereProviders = [ - 'HERE.normalDay', - 'HERE.normalNight', - 'HERE.hybridDay', - 'HERE.terrainDay'] diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts index 2e3927fdc4..7c8f50e13f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts @@ -120,7 +120,6 @@ export class Marker { [this.data, this.settings.markerImages, this.dataSources, this.data.dsIndex]) : this.settings.currentImage; const currentColor = tinycolor(this.settings.useColorFunction ? safeExecute(this.settings.colorFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.color).toHex(); - if (currentImage && currentImage.url) { aspectCache(currentImage.url).subscribe( (aspect) => { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts index 4f1fc9dd72..6954d122c2 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/polygon.ts @@ -53,8 +53,9 @@ export class Polygon { } updateTooltip(data: DatasourceData) { - const pattern = this.settings.useTooltipFunction ? - safeExecute(this.settings.tooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern; + const pattern = this.settings.usePolygonTooltipFunction ? + safeExecute(this.settings.polygonTooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : + this.settings.polygonTooltipPattern; this.tooltip.setContent(parseWithTranslation.parseTemplate(pattern, data, true)); } @@ -71,10 +72,12 @@ export class Polygon { this.map.removeLayer(this.leafletPoly); } - updatePolygonColor(settings) { + updatePolygonColor(settings: PolygonSettings) { + const color = settings.usePolygonColorFunction ? + safeExecute(settings.polygonColorFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.polygonColor; const style: L.PathOptions = { fill: true, - fillColor: settings.polygonColor, + fillColor: color, color: settings.polygonStrokeColor, weight: settings.polygonStrokeWeight, fillOpacity: settings.polygonOpacity, diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts index fce2fb3644..ac46c2772b 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/image-map.ts @@ -109,13 +109,13 @@ export class ImageMap extends LeafletMap { lastCenterPos.y /= prevHeight; this.updateBounds(updateImage, lastCenterPos); this.map.invalidateSize(true); - // TODO: need add update marker position + this.updateMarkers(this.markersData); } } } } - fitBounds(bounds: LatLngBounds, useDefaultZoom = false, padding?: LatLngTuple) { } + fitBounds(bounds: LatLngBounds, padding?: LatLngTuple) { } initMap(updateImage?) { if (!this.map && this.aspect > 0) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts index de155bf2b4..4f8808e3e4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts @@ -546,6 +546,20 @@ export const mapPolygonSchema = type: 'boolean', default: false }, + polygonTooltipPattern: { + title: 'Tooltip (for ex. \'Text ${keyName} units.\' or Link text\')', + type: 'string', + default: '${entityName}

TimeStamp: ${ts:7}' + }, + usePolygonTooltipFunction: { + title: 'Use polygon tooltip function', + type: 'boolean', + default: false + }, + polygonTooltipFunction: { + title: 'Polygon tooltip function: f(data, dsData, dsIndex)', + type: 'string' + }, usePolygonColorFunction: { title: 'Use polygon color function', type: 'boolean', @@ -570,7 +584,15 @@ export const mapPolygonSchema = key: 'polygonStrokeColor', type: 'color' }, - 'polygonStrokeOpacity', 'polygonStrokeWeight', 'usePolygonColorFunction', 'showPolygonTooltip', + 'polygonStrokeOpacity', 'polygonStrokeWeight', 'showPolygonTooltip', + { + key: 'polygonTooltipPattern', + type: 'textarea' + }, 'usePolygonTooltipFunction', { + key: 'polygonTooltipFunction', + type: 'javascript' + }, + 'usePolygonColorFunction', { key: 'polygonColorFunction', type: 'javascript' @@ -710,6 +732,161 @@ export const imageMapSettingsSchema = ] }; +export const pathSchema = +{ + schema: { + title: 'Trip Animation Path Configuration', + type: 'object', + properties: { + color: { + title: 'Path color', + type: 'string' + }, + strokeWeight: { + title: 'Stroke weight', + type: 'number', + default: 2 + }, + strokeOpacity: { + title: 'Stroke opacity', + type: 'number', + default: 1 + }, + useColorFunction: { + title: 'Use path color function', + type: 'boolean', + default: false + }, + colorFunction: { + title: 'Path color function: f(data, dsData, dsIndex)', + type: 'string' + }, + usePolylineDecorator: { + title: 'Use path decorator', + type: 'boolean', + default: false + }, + decoratorSymbol: { + title: 'Decorator symbol', + type: 'string', + default: 'arrowHead' + }, + decoratorSymbolSize: { + title: 'Decorator symbol size (px)', + type: 'number', + default: 10 + }, + useDecoratorCustomColor: { + title: 'Use path decorator custom color', + type: 'boolean', + default: false + }, + decoratorCustomColor: { + title: 'Decorator custom color', + type: 'string', + default: '#000' + }, + decoratorOffset: { + title: 'Decorator offset', + type: 'string', + default: '20px' + }, + endDecoratorOffset: { + title: 'End decorator offset', + type: 'string', + default: '20px' + }, + decoratorRepeat: { + title: 'Decorator repeat', + type: 'string', + default: '20px' + } + }, + required: [] + }, + form: [ + { + key: 'color', + type: 'color' + }, 'useColorFunction', { + key: 'colorFunction', + type: 'javascript' + }, 'strokeWeight', 'strokeOpacity', + 'usePolylineDecorator', { + key: 'decoratorSymbol', + type: 'rc-select', + multiple: false, + items: [{ + value: 'arrowHead', + label: 'Arrow' + }, { + value: 'dash', + label: 'Dash' + }] + }, 'decoratorSymbolSize', 'useDecoratorCustomColor', { + key: 'decoratorCustomColor', + type: 'color' + }, { + key: 'decoratorOffset', + type: 'textarea' + }, { + key: 'endDecoratorOffset', + type: 'textarea' + }, { + key: 'decoratorRepeat', + type: 'textarea' + } + ] +}; + +export const pointSchema = +{ + schema: { + title: 'Trip Animation Path Configuration', + type: 'object', + properties: { + showPoints: { + title: 'Show points', + type: 'boolean', + default: false + }, + pointColor: { + title: 'Point color', + type: 'string' + }, + pointSize: { + title: 'Point size (px)', + type: 'number', + default: 10 + }, + usePointAsAnchor: { + title: 'Use point as anchor', + type: 'boolean', + default: false + }, + pointAsAnchorFunction: { + title: 'Point as anchor function: f(data, dsData, dsIndex)', + type: 'string' + }, + pointTooltipOnRightPanel: { + title: 'Independant point tooltip', + type: 'boolean', + default: true + }, + }, + required: [] + }, + form: [ + 'showPoints', { + key: 'pointColor', + type: 'color' + }, 'pointSize', 'usePointAsAnchor', { + key: 'pointAsAnchorFunction', + type: 'javascript' + }, 'pointTooltipOnRightPanel', + ] +}; + export const mapProviderSchema = { schema: { @@ -755,7 +932,6 @@ export const mapProviderSchema = ] }; - export const tripAnimationSchema = { schema: { title: 'Openstreet Map Configuration', @@ -776,11 +952,6 @@ export const tripAnimationSchema = { type: 'string', default: 'longitude' }, - polKeyName: { - title: 'Polygon key name', - type: 'string', - default: 'coordinates' - }, showLabel: { title: 'Show label', type: 'boolean', @@ -834,148 +1005,6 @@ export const tripAnimationSchema = { title: 'Tooltip function: f(data, dsData, dsIndex)', type: 'string' }, - color: { - title: 'Path color', - type: 'string' - }, - strokeWeight: { - title: 'Stroke weight', - type: 'number', - default: 2 - }, - strokeOpacity: { - title: 'Stroke opacity', - type: 'number', - default: 1 - }, - useColorFunction: { - title: 'Use path color function', - type: 'boolean', - default: false - }, - colorFunction: { - title: 'Path color function: f(data, dsData, dsIndex)', - type: 'string' - }, - usePolylineDecorator: { - title: 'Use path decorator', - type: 'boolean', - default: false - }, - decoratorSymbol: { - title: 'Decorator symbol', - type: 'string', - default: 'arrowHead' - }, - decoratorSymbolSize: { - title: 'Decorator symbol size (px)', - type: 'number', - default: 10 - }, - useDecoratorCustomColor: { - title: 'Use path decorator custom color', - type: 'boolean', - default: false - }, - decoratorCustomColor: { - title: 'Decorator custom color', - type: 'string', - default: '#000' - }, - decoratorOffset: { - title: 'Decorator offset', - type: 'string', - default: '20px' - }, - endDecoratorOffset: { - title: 'End decorator offset', - type: 'string', - default: '20px' - }, - decoratorRepeat: { - title: 'Decorator repeat', - type: 'string', - default: '20px' - }, - showPolygon: { - title: 'Show polygon', - type: 'boolean', - default: false - }, - polygonTooltipPattern: { - title: 'Tooltip (for ex. \'Text ${keyName} units.\' or Link text\')', - type: 'string', - default: '${entityName}

TimeStamp: ${ts:7}' - }, - usePolygonTooltipFunction: { - title: 'Use polygon tooltip function', - type: 'boolean', - default: false - }, - polygonTooltipFunction: { - title: 'Polygon tooltip function: f(data, dsData, dsIndex)', - type: 'string' - }, - polygonColor: { - title: 'Polygon color', - type: 'string' - }, - polygonOpacity: { - title: 'Polygon opacity', - type: 'number', - default: 0.5 - }, - polygonStrokeColor: { - title: 'Polygon border color', - type: 'string' - }, - polygonStrokeOpacity: { - title: 'Polygon border opacity', - type: 'number', - default: 1 - }, - polygonStrokeWeight: { - title: 'Polygon border weight', - type: 'number', - default: 1 - }, - usePolygonColorFunction: { - title: 'Use polygon color function', - type: 'boolean', - default: false - }, - polygonColorFunction: { - title: 'Polygon Color function: f(data, dsData, dsIndex)', - type: 'string' - }, - showPoints: { - title: 'Show points', - type: 'boolean', - default: false - }, - pointColor: { - title: 'Point color', - type: 'string' - }, - pointSize: { - title: 'Point size (px)', - type: 'number', - default: 10 - }, - usePointAsAnchor: { - title: 'Use point as anchor', - type: 'boolean', - default: false - }, - pointAsAnchorFunction: { - title: 'Point as anchor function: f(data, dsData, dsIndex)', - type: 'string' - }, - pointTooltipOnRightPanel: { - title: 'Independant point tooltip', - type: 'boolean', - default: true - }, autocloseTooltip: { title: 'Auto-close point popup', type: 'boolean', @@ -1015,111 +1044,35 @@ export const tripAnimationSchema = { }, required: [] }, - form: [{ - key: 'mapProvider', - type: 'rc-select', - multiple: false, - items: [{ - value: 'OpenStreetMap.Mapnik', - label: 'OpenStreetMap.Mapnik (Default)' - }, { - value: 'OpenStreetMap.BlackAndWhite', - label: 'OpenStreetMap.BlackAndWhite' - }, { - value: 'OpenStreetMap.HOT', - label: 'OpenStreetMap.HOT' - }, { - value: 'Esri.WorldStreetMap', - label: 'Esri.WorldStreetMap' - }, { - value: 'Esri.WorldTopoMap', - label: 'Esri.WorldTopoMap' - }, { - value: 'CartoDB.Positron', - label: 'CartoDB.Positron' - }, { - value: 'CartoDB.DarkMatter', - label: 'CartoDB.DarkMatter' - }] - }, 'normalizationStep', 'latKeyName', 'lngKeyName', 'polKeyName', 'showLabel', 'label', 'useLabelFunction', { + form: ['normalizationStep', 'latKeyName', 'lngKeyName', 'showLabel', 'label', 'useLabelFunction', { key: 'labelFunction', type: 'javascript' }, 'showTooltip', { - key: 'tooltipColor', - type: 'color' - }, { - key: 'tooltipFontColor', - type: 'color' - }, 'tooltipOpacity', { - key: 'tooltipPattern', - type: 'textarea' - }, 'useTooltipFunction', { - key: 'tooltipFunction', - type: 'javascript' - }, { - key: 'color', - type: 'color' - }, 'useColorFunction', { - key: 'colorFunction', - type: 'javascript' - }, 'usePolylineDecorator', { - key: 'decoratorSymbol', - type: 'rc-select', - multiple: false, - items: [{ - value: 'arrowHead', - label: 'Arrow' + key: 'tooltipColor', + type: 'color' + }, { + key: 'tooltipFontColor', + type: 'color' + }, 'tooltipOpacity', { + key: 'tooltipPattern', + type: 'textarea' + }, 'useTooltipFunction', { + key: 'tooltipFunction', + type: 'javascript' + }, 'autocloseTooltip', { + key: 'markerImage', + type: 'image' + }, 'markerImageSize', 'rotationAngle', 'useMarkerImageFunction', + { + key: 'markerImageFunction', + type: 'javascript' }, { - value: 'dash', - label: 'Dash' + key: 'markerImages', + items: [ + { + key: 'markerImages[]', + type: 'image' + } + ] }] - }, 'decoratorSymbolSize', 'useDecoratorCustomColor', { - key: 'decoratorCustomColor', - type: 'color' - }, { - key: 'decoratorOffset', - type: 'textarea' - }, { - key: 'endDecoratorOffset', - type: 'textarea' - }, { - key: 'decoratorRepeat', - type: 'textarea' - }, 'strokeWeight', 'strokeOpacity', 'showPolygon', { - key: 'polygonTooltipPattern', - type: 'textarea' - }, 'usePolygonTooltipFunction', { - key: 'polygonTooltipFunction', - type: 'javascript' - }, { - key: 'polygonColor', - type: 'color' - }, 'polygonOpacity', { - key: 'polygonStrokeColor', - type: 'color' - }, 'polygonStrokeOpacity', 'polygonStrokeWeight', 'usePolygonColorFunction', { - key: 'polygonColorFunction', - type: 'javascript' - }, 'showPoints', { - key: 'pointColor', - type: 'color' - }, 'pointSize', 'usePointAsAnchor', { - key: 'pointAsAnchorFunction', - type: 'javascript' - }, 'pointTooltipOnRightPanel', 'autocloseTooltip', { - key: 'markerImage', - type: 'image' - }, 'markerImageSize', 'rotationAngle', 'useMarkerImageFunction', - { - key: 'markerImageFunction', - type: 'javascript' - }, { - key: 'markerImages', - items: [ - { - key: 'markerImages[]', - type: 'image' - } - ] - }] } diff --git a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html index b973284b53..9449e3aba9 100644 --- a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html @@ -32,6 +32,6 @@ [ngStyle]="{'background-color': settings.tooltipColor, 'opacity': settings.tooltipOpacity, 'color': settings.tooltipFontColor}">
-
\ No newline at end of file diff --git a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts index 47b7e7f743..7dfec08c9f 100644 --- a/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts @@ -21,9 +21,9 @@ import { interpolateOnPointSegment } from 'leaflet-geometryutil'; import { AfterViewInit, ChangeDetectorRef, Component, Input, OnInit, SecurityContext, ViewChild } from '@angular/core'; import { MapWidgetController, TbMapWidgetV2 } from '../lib/maps/map-widget2'; -import { MapProviders } from '../lib/maps/map-models'; -import { initSchema, addToSchema, addGroupInfo } from '@app/core/schema-utils'; -import { tripAnimationSchema } from '../lib/maps/schemes'; +import { MapProviders, FormattedData } from '../lib/maps/map-models'; +import { initSchema, addToSchema, addGroupInfo, addCondition } from '@app/core/schema-utils'; +import { tripAnimationSchema, mapPolygonSchema, pathSchema, pointSchema } from '../lib/maps/schemes'; import { DomSanitizer } from '@angular/platform-browser'; import { WidgetContext } from '@app/modules/home/models/widget-component.models'; import { findAngle, getRatio, parseArray, parseWithTranslation, safeExecute } from '../lib/maps/maps-utils'; @@ -58,13 +58,21 @@ export class TripAnimationComponent implements OnInit, AfterViewInit { label; minTime; maxTime; + anchors = []; + useAnchors = false; static getSettingsSchema(): JsonSettingsSchema { const schema = initSchema(); - addToSchema(schema, TbMapWidgetV2.getProvidersSchema()); + addToSchema(schema, TbMapWidgetV2.getProvidersSchema(null, true)); addGroupInfo(schema, 'Map Provider Settings'); addToSchema(schema, tripAnimationSchema); addGroupInfo(schema, 'Trip Animation Settings'); + addToSchema(schema, pathSchema); + addGroupInfo(schema, 'Path Settings'); + addToSchema(schema, addCondition(pointSchema, 'model.showPoints === true', ['showPoints'])); + addGroupInfo(schema, 'Path Points Settings'); + addToSchema(schema, addCondition(mapPolygonSchema, 'model.showPolygon === true', ['showPolygon'])); + addGroupInfo(schema, 'Polygon Settings'); return schema; } @@ -78,14 +86,15 @@ export class TripAnimationComponent implements OnInit, AfterViewInit { rotationAngle: 0 } this.settings = { ...settings, ...this.ctx.settings }; + this.useAnchors = this.settings.usePointAsAnchor && this.settings.showPoints; + this.settings.fitMapBounds = true; + this.normalizationStep = this.settings.normalizationStep; const subscription = this.ctx.subscriptions[Object.keys(this.ctx.subscriptions)[0]]; - if (subscription) subscription.callbacks.onDataUpdated = (updated) => { + if (subscription) subscription.callbacks.onDataUpdated = () => { this.historicalData = parseArray(this.ctx.data); this.activeTrip = this.historicalData[0][0]; this.calculateIntervals(); this.timeUpdated(this.intervals[0]); - this.mapWidget.map.updatePolylines(this.interpolatedData.map(ds => _.values(ds))); - this.mapWidget.map.map?.invalidateSize(); this.cd.detectChanges(); } @@ -104,9 +113,17 @@ export class TripAnimationComponent implements OnInit, AfterViewInit { this.calcLabel(); this.calcTooltip(); if (this.mapWidget) { + this.mapWidget.map.updatePolylines(this.interpolatedData.map(ds => _.values(ds))); if (this.settings.showPolygon) { this.mapWidget.map.updatePolygons(this.interpolatedData); } + if (this.settings.showPoints) { + this.mapWidget.map.updatePoints(this.historicalData[0], this.calcTooltip); + this.anchors = this.historicalData[0] + .filter(data => + this.settings.usePointAsAnchor || + safeExecute(this.settings.pointAsAnchorFunction, [this.historicalData, data, data.dsIndex])).map(data => data.time); + } this.mapWidget.map.updateMarkers(currentPosition); } } @@ -117,23 +134,29 @@ export class TripAnimationComponent implements OnInit, AfterViewInit { calculateIntervals() { this.historicalData.forEach((dataSource, index) => { this.intervals = []; - for (let time = dataSource[0]?.time; time < dataSource[dataSource.length - 1]?.time; time += this.normalizationStep) { this.intervals.push(time); } - this.intervals.push(dataSource[dataSource.length - 1]?.time); this.interpolatedData[index] = this.interpolateArray(dataSource, this.intervals); }); } - calcTooltip() { - const data = { ...this.activeTrip, maxTime: this.maxTime, minTime: this.minTime } - const tooltipText: string = this.settings.useTooltipFunction ? + calcTooltip = (point?: FormattedData, setTooltip = true) => { + if (!point) { + point = this.activeTrip; + } + const data = { ...point, maxTime: this.maxTime, minTime: this.minTime } + const tooltipPattern: string = this.settings.useTooltipFunction ? safeExecute(this.settings.tooolTipFunction, [data, this.historicalData, 0]) : this.settings.tooltipPattern; - this.mainTooltip = this.sanitizer.sanitize( - SecurityContext.HTML, (parseWithTranslation.parseTemplate(tooltipText, data, true))); + const tooltipText = parseWithTranslation.parseTemplate(tooltipPattern, data, true); + if (setTooltip) { + this.mainTooltip = this.sanitizer.sanitize( + SecurityContext.HTML, tooltipText); + this.cd.detectChanges(); + } + return tooltipText; } calcLabel() { diff --git a/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.ts b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.ts index 252720033c..a15b892349 100644 --- a/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.ts +++ b/ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.ts @@ -28,6 +28,8 @@ export class HistorySelectorComponent implements OnInit, OnChanges { @Input() settings: HistorySelectSettings @Input() intervals = []; + @Input() anchors = []; + @Input() useAnchors = false; @Output() timeUpdated: EventEmitter = new EventEmitter(); @@ -56,7 +58,7 @@ export class HistorySelectorComponent implements OnInit, OnChanges { this.interval = interval(1000 / this.speed) .pipe( filter(() => this.playing)).subscribe(() => { - this.index++; + this.index++; if (this.index < this.maxTimeIndex) { this.cd.detectChanges(); this.timeUpdated.emit(this.intervals[this.index]); @@ -91,14 +93,24 @@ export class HistorySelectorComponent implements OnInit, OnChanges { moveNext() { if (this.index < this.maxTimeIndex) { - this.index++; + if (this.useAnchors) { + const anchorIndex = this.findIndex(this.intervals[this.index], this.anchors)+1; + this.index = this.findIndex(this.anchors[anchorIndex], this.intervals); + } + else + this.index++; } this.pause(); } movePrev() { if (this.index > this.minTimeIndex) { - this.index++; + if (this.useAnchors) { + const anchorIndex = this.findIndex(this.intervals[this.index], this.anchors) - 1; + this.index = this.findIndex(this.anchors[anchorIndex], this.intervals); + } + else + this.index--; } this.pause(); } @@ -113,6 +125,14 @@ export class HistorySelectorComponent implements OnInit, OnChanges { this.pause(); } + findIndex(value, array: any[]) { + let i = 0; + while (array[i] < value) { + i++; + }; + return i; + } + changeIndex() { this.timeUpdated.emit(this.intervals[this.index]); }