Browse Source

Map/3.0 (#2664)

* add base map infrastructure

* add leaflet css

* add tencent map

* add google maps support

* added image map support

* refactor schemes

* here maps support && WIP on markers

* add simple marker suppor

* data update & polyline support

* map bouds support

* add some settings support

* add map provider select to settings

* labels support

* WIP on trip animation widget

* WIP on history control and route interpolation

* trip-animation map provider & custom markers

* comleted track marker & history controls

* add license headers

* label fix & tooltips support

* WIP on polygons

* marker dropping support

* add polygon support

* add label to trip animation

* WIP on tooltips

* lint anf typed leaflet AddMarker

* some typing and poly improvements

* add typing

* add marker creation

* update proxy

* save position fix

* add bounds padding

* update map widget bendle && bugfixes

* update marker placement widget

* add licenses

* reomove log

* fix sizes

* entity and map fixes

* Fix tile server support  form OSM and zoom level fix

* add support custom actions

* map fixes

Co-authored-by: Artem Halushko <ahalushko@thingboards.io>
Co-authored-by: Adsumus <artemtv42@gmail.com>
pull/2667/head
ArtemHalushko 7 years ago
committed by GitHub
parent
commit
241edc1d2b
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      ui-ngx/angular.json
  2. 14
      ui-ngx/package-lock.json
  3. 1
      ui-ngx/package.json
  4. 59
      ui-ngx/src/app/core/utils.ts
  5. 83
      ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts
  6. 37
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts
  7. 5
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts
  8. 78
      ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts
  9. 25
      ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts
  10. 4
      ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss
  11. 49
      ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts
  12. 7
      ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts
  13. 153
      ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts
  14. 10
      ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html
  15. 8
      ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.scss
  16. 42
      ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts
  17. 23
      ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.html
  18. 186
      ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.scss
  19. 4
      ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.ts
  20. 4
      ui-ngx/src/app/shared/pipe/template.pipe.ts

4
ui-ngx/angular.json

@ -39,7 +39,9 @@
"node_modules/rc-select/assets/index.less", "node_modules/rc-select/assets/index.less",
"node_modules/jstree-bootstrap-theme/dist/themes/proton/style.min.css", "node_modules/jstree-bootstrap-theme/dist/themes/proton/style.min.css",
"node_modules/leaflet/dist/leaflet.css", "node_modules/leaflet/dist/leaflet.css",
"src/app/modules/home/components/widget/lib/maps/markers.scss" "src/app/modules/home/components/widget/lib/maps/markers.scss",
"node_modules/leaflet.markercluster/dist/MarkerCluster.css",
"node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css"
], ],
"stylePreprocessorOptions": { "stylePreprocessorOptions": {
"includePaths": [ "includePaths": [

14
ui-ngx/package-lock.json

@ -1816,8 +1816,7 @@
"@types/geojson": { "@types/geojson": {
"version": "7946.0.7", "version": "7946.0.7",
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.7.tgz",
"integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==", "integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ=="
"dev": true
}, },
"@types/glob": { "@types/glob": {
"version": "7.1.1", "version": "7.1.1",
@ -1881,7 +1880,6 @@
"version": "1.5.12", "version": "1.5.12",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.5.12.tgz", "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.5.12.tgz",
"integrity": "sha512-61HRMIng+bWvnnAIqUWLBlrd/TQZc4gU+gN1JL4K47EDtwIrcMEhWgi7PdcpbG1YmpH4F0EfOimkvV82gJIl9w==", "integrity": "sha512-61HRMIng+bWvnnAIqUWLBlrd/TQZc4gU+gN1JL4K47EDtwIrcMEhWgi7PdcpbG1YmpH4F0EfOimkvV82gJIl9w==",
"dev": true,
"requires": { "requires": {
"@types/geojson": "*" "@types/geojson": "*"
} }
@ -1895,6 +1893,14 @@
"@types/leaflet": "*" "@types/leaflet": "*"
} }
}, },
"@types/leaflet.markercluster": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@types/leaflet.markercluster/-/leaflet.markercluster-1.4.2.tgz",
"integrity": "sha512-QQ//hevAxMH2dlRQdRre7V/1G+TbtuDtZnZF/75TNwVIgklrsQVCIcS/cvLsl7UUryfPJ6xmoYHfFzK5iGVgpg==",
"requires": {
"@types/leaflet": "*"
}
},
"@types/lodash": { "@types/lodash": {
"version": "4.14.150", "version": "4.14.150",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.150.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.150.tgz",
@ -8605,7 +8611,7 @@
"integrity": "sha512-4O3GWAYJaauMCILm07weko2rHA8a4kjn7+8Lg4s1d7SxwS/3IpkVD/GljbRrIJ1c1W/XGJ3GbuK7RyYZEJChhw==" "integrity": "sha512-4O3GWAYJaauMCILm07weko2rHA8a4kjn7+8Lg4s1d7SxwS/3IpkVD/GljbRrIJ1c1W/XGJ3GbuK7RyYZEJChhw=="
}, },
"ngx-flowchart": { "ngx-flowchart": {
"version": "git://github.com/thingsboard/ngx-flowchart.git#a4157b0eef2eb3646ef920447c7b06b39d54f87f", "version": "git://github.com/thingsboard/ngx-flowchart.git#97a77477ca8579becf0e3a07866046b4536fe30a",
"from": "git://github.com/thingsboard/ngx-flowchart.git#master", "from": "git://github.com/thingsboard/ngx-flowchart.git#master",
"requires": { "requires": {
"tslib": "^1.10.0" "tslib": "^1.10.0"

1
ui-ngx/package.json

@ -108,6 +108,7 @@
"@types/jstree": "^3.3.39", "@types/jstree": "^3.3.39",
"@types/jszip": "^3.1.7", "@types/jszip": "^3.1.7",
"@types/leaflet": "^1.5.12", "@types/leaflet": "^1.5.12",
"@types/leaflet.markercluster": "^1.4.2",
"@types/leaflet-polylinedecorator": "^1.6.0", "@types/leaflet-polylinedecorator": "^1.6.0",
"@types/lodash": "^4.14.150", "@types/lodash": "^4.14.150",
"@types/raphael": "^2.3.0", "@types/raphael": "^2.3.0",

59
ui-ngx/src/app/core/utils.ts

@ -15,7 +15,7 @@
/// ///
import _ from 'lodash'; import _ from 'lodash';
import { Observable, Subject, from, fromEvent, of } from 'rxjs'; import { Observable, Subject, fromEvent, of } from 'rxjs';
import { finalize, share, map } from 'rxjs/operators'; import { finalize, share, map } from 'rxjs/operators';
import base64js from 'base64-js'; import base64js from 'base64-js';
@ -430,7 +430,7 @@ export function getDescendantProp(obj: any, path: string): any {
export function imageLoader(imageUrl: string): Observable<HTMLImageElement> { export function imageLoader(imageUrl: string): Observable<HTMLImageElement> {
const image = new Image(); const image = new Image();
const imageLoad$ = fromEvent(image, 'load').pipe(map(event => image)); const imageLoad$ = fromEvent(image, 'load').pipe(map(() => image));
image.src = imageUrl; image.src = imageUrl;
return imageLoad$; return imageLoad$;
} }
@ -524,36 +524,49 @@ export function parseFunction(source: any, params: string[] = []): Function {
return res; return res;
} }
export function parseTemplate(template: string, data: object) { export function parseTemplate(template: string, data: object, translateFn?: (key: string) => string) {
let res = ''; let res = '';
let variables = '';
try { try {
let variables = ''; if (template.match(/<link-act/g)) {
const expressions = template template = template.replace(/<link-act/g, '<a').replace(/link-act>/g, 'a>').replace(/name=(\'|")(.*?)(\'|")/g, `class='tb-custom-action' id='$2'`);
.match(/\{(.*?)\}/g) // find expressions }
.map(exp => exp.replace(/{|}/g, '')) // remove brackets if (template.includes('i18n')) {
.map(exp => exp.split(':')) const translateRegexp = /\{i18n:(.*?)\}/;
.map(arr => { template.match(new RegExp(translateRegexp.source, translateRegexp.flags + 'g')).forEach(match => {
variables += `let ${arr[0]} = ''; `; template = template.replace(match, translateFn(match.match(translateRegexp)[1]));
return arr; });
}) }
.filter(arr => !!arr[1]) // filter expressions without format const expressions = template.match(/\{(.*?)\}/g);
.reduce((res, current) => { if (expressions) {
res[current[0]] = current[1]; const clearMatches = template.match(/(?<=\{)(.+?)(?=(\}|\:))/g);
return res; for (const key in data) {
}, {}); if (!key.includes('|'))
variables += `let ${key} = '${clearMatches[key] ? padValue(data[key], +clearMatches[key]) : data[key]}';`;
for (const key in data) { }
if (!key.includes('|')) template = template.replace(/\:\d+\}/g, '}');
variables += `${key} = '${expressions[key] ? padValue(data[key], +expressions[key]) : data[key]}';`; res = safeExecute(parseFunction(variables + ' return' + '`' + template + '`'));
} }
template = template.replace(/:\d+}/g, '}'); else res = template;
res = safeExecute(parseFunction(variables + 'return' + '`' + template + '`'));
} }
catch (ex) { catch (ex) {
console.log(ex, variables, template)
} }
return res; return res;
} }
export let parseWithTranslation = {
translate(): string {
throw console.error('Translate not assigned');
},
parseTemplate(template: string, data: object, forceTranslate = false): string {
return parseTemplate(forceTranslate ? this.translate(template) : template, data, this?.translate);
},
setTranslate(translateFn: (key: string, defaultTranslation?: string) => string) {
this.translate = translateFn;
}
}
export function padValue(val: any, dec: number): string { export function padValue(val: any, dec: number): string {
let strVal; let strVal;
let n; let n;

83
ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts

@ -14,12 +14,12 @@
/// limitations under the License. /// limitations under the License.
/// ///
import L, { LatLngTuple } from 'leaflet'; import L, { LatLngTuple, LatLngBounds, Point } from 'leaflet';
import 'leaflet-providers'; import 'leaflet-providers';
import 'leaflet.markercluster/dist/MarkerCluster.css' import 'leaflet.markercluster/dist/MarkerCluster.css';
import 'leaflet.markercluster/dist/MarkerCluster.Default.css' import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
import 'leaflet.markercluster/dist/leaflet.markercluster' import LM from 'leaflet.markercluster/dist/leaflet.markercluster';
import { MapSettings, MarkerSettings, FormattedData, UnitedMapSettings, PolygonSettings, PolylineSettings } from './map-models'; import { MapSettings, MarkerSettings, FormattedData, UnitedMapSettings, PolygonSettings, PolylineSettings } from './map-models';
import { Marker } from './markers'; import { Marker } from './markers';
@ -34,7 +34,7 @@ export default abstract class LeafletMap {
markers: Map<string, Marker> = new Map(); markers: Map<string, Marker> = new Map();
polylines: Map<string, Polyline> = new Map(); polylines: Map<string, Polyline> = new Map();
polygons: Map<string, Polygon> = new Map(); polygons: Map<string, Polygon> = new Map();
dragMode = true; dragMode = false;
map: L.Map; map: L.Map;
map$: BehaviorSubject<L.Map> = new BehaviorSubject(null); map$: BehaviorSubject<L.Map> = new BehaviorSubject(null);
ready$: Observable<L.Map> = this.map$.pipe(filter(map => !!map)); ready$: Observable<L.Map> = this.map$.pipe(filter(map => !!map));
@ -43,6 +43,7 @@ export default abstract class LeafletMap {
bounds: L.LatLngBounds; bounds: L.LatLngBounds;
newMarker: L.Marker; newMarker: L.Marker;
datasources: FormattedData[]; datasources: FormattedData[];
markersCluster: LM.markerClusterGroup;
constructor(public $container: HTMLElement, options: UnitedMapSettings) { constructor(public $container: HTMLElement, options: UnitedMapSettings) {
this.options = options; this.options = options;
@ -50,13 +51,38 @@ export default abstract class LeafletMap {
public initSettings(options: MapSettings) { public initSettings(options: MapSettings) {
const { initCallback, const { initCallback,
disableScrollZooming, }: MapSettings = options; disableScrollZooming,
useClusterMarkers,
zoomOnClick,
showCoverageOnHover,
removeOutsideVisibleBounds,
animate,
chunkedLoading,
maxClusterRadius,
maxZoom }: MapSettings = options;
if (disableScrollZooming) { if (disableScrollZooming) {
this.map.scrollWheelZoom.disable(); this.map.scrollWheelZoom.disable();
} }
if (initCallback) { if (initCallback) {
setTimeout(options.initCallback, 0); setTimeout(options.initCallback, 0);
} }
if (useClusterMarkers) {
const clusteringSettings: LM.MarkerClusterGroupOptions = {
zoomToBoundsOnClick: zoomOnClick,
showCoverageOnHover,
removeOutsideVisibleBounds,
animate,
chunkedLoading
};
if (maxClusterRadius && maxClusterRadius > 0) {
clusteringSettings.maxClusterRadius = Math.floor(maxClusterRadius);
}
if (maxZoom && maxZoom >= 0 && maxZoom < 19) {
clusteringSettings.disableClusteringAtZoom = Math.floor(maxZoom);
}
this.markersCluster = LM.markerClusterGroup(clusteringSettings);
this.ready$.subscribe(map => map.addLayer(this.markersCluster));
}
} }
addMarkerControl() { addMarkerControl() {
@ -65,7 +91,7 @@ export default abstract class LeafletMap {
let addMarker: L.Control; let addMarker: L.Control;
this.map.on('mouseup', (e: L.LeafletMouseEvent) => { this.map.on('mouseup', (e: L.LeafletMouseEvent) => {
mousePositionOnMap = e.latlng; mousePositionOnMap = e.latlng;
}) });
const dragListener = (e: L.DragEndEvent) => { const dragListener = (e: L.DragEndEvent) => {
if (e.type === 'dragend' && mousePositionOnMap) { if (e.type === 'dragend' && mousePositionOnMap) {
const newMarker = L.marker(mousePositionOnMap).addTo(this.map); const newMarker = L.marker(mousePositionOnMap).addTo(this.map);
@ -80,10 +106,17 @@ export default abstract class LeafletMap {
this.saveMarkerLocation(updatedEnttity); this.saveMarkerLocation(updatedEnttity);
this.map.removeLayer(newMarker); this.map.removeLayer(newMarker);
this.deleteMarker(ds.entityName); this.deleteMarker(ds.entityName);
this.createMarker(ds.entityName, updatedEnttity, this.datasources, this.options, false); this.createMarker(ds.entityName, updatedEnttity, this.datasources, this.options);
} }
datasourcesList.append(dsItem); datasourcesList.append(dsItem);
}) });
const deleteBtn = document.createElement('a');
deleteBtn.appendChild(document.createTextNode('Delete position'));
deleteBtn.setAttribute('color', 'red');
deleteBtn.onclick = () => {
this.map.removeLayer(newMarker);
}
datasourcesList.append(deleteBtn);
const popup = L.popup(); const popup = L.popup();
popup.setContent(datasourcesList); popup.setContent(datasourcesList);
newMarker.bindPopup(popup).openPopup(); newMarker.bindPopup(popup).openPopup();
@ -96,6 +129,7 @@ export default abstract class LeafletMap {
img.src = `assets/add_location.svg`; img.src = `assets/add_location.svg`;
img.style.width = '32px'; img.style.width = '32px';
img.style.height = '32px'; img.style.height = '32px';
img.title = 'Drag and drop to add marker';
img.onclick = this.dragMarker; img.onclick = this.dragMarker;
img.draggable = true; img.draggable = true;
const draggableImg = new L.Draggable(img); const draggableImg = new L.Draggable(img);
@ -106,13 +140,10 @@ export default abstract class LeafletMap {
onRemove(map) { onRemove(map) {
}, },
dragMarker: this.dragMarker dragMarker: this.dragMarker
} as any); } as any);
L.control.addMarker = (opts) => { L.control.addMarker = (opts) => {
return new L.Control.AddMarker(opts); return new L.Control.AddMarker(opts);
} }
addMarker = L.control.addMarker({ position: 'topright' }).addTo(this.map); addMarker = L.control.addMarker({ position: 'topright' }).addTo(this.map);
} }
} }
@ -164,9 +195,9 @@ export default abstract class LeafletMap {
return this.map.getCenter(); return this.map.getCenter();
} }
fitBounds(bounds, useDefaultZoom = false) { fitBounds(bounds: LatLngBounds, useDefaultZoom = false, padding?: LatLngTuple) {
if (bounds.isValid()) { if (bounds.isValid()) {
if ((this.options.dontFitMapBounds || useDefaultZoom) && this.options.defaultZoomLevel) { if ((!this.options.fitMapBounds || useDefaultZoom) && this.options.defaultZoomLevel) {
this.map.setZoom(this.options.defaultZoomLevel, { animate: false }); this.map.setZoom(this.options.defaultZoomLevel, { animate: false });
this.map.panTo(bounds.getCenter(), { animate: false }); this.map.panTo(bounds.getCenter(), { animate: false });
} else { } else {
@ -175,7 +206,7 @@ export default abstract class LeafletMap {
this.map.setZoom(this.options.minZoomLevel, { animate: false }); this.map.setZoom(this.options.minZoomLevel, { animate: false });
} }
}); });
this.map.fitBounds(bounds, { padding: [50, 50], animate: false }); this.map.fitBounds(bounds, { padding: padding || [50, 50], animate: false });
} }
this.bounds = this.bounds.extend(bounds); this.bounds = this.bounds.extend(bounds);
} }
@ -193,8 +224,8 @@ export default abstract class LeafletMap {
convertToCustomFormat(position: L.LatLng): object { convertToCustomFormat(position: L.LatLng): object {
return { return {
[this.options.latKeyName]: position.lat, [this.options.latKeyName]: position.lat % 180,
[this.options.lngKeyName]: position.lng [this.options.lngKeyName]: position.lng % 180
} }
} }
@ -225,11 +256,17 @@ export default abstract class LeafletMap {
this.saveMarkerLocation({ ...data, ...this.convertToCustomFormat(e.target._latlng) }); this.saveMarkerLocation({ ...data, ...this.convertToCustomFormat(e.target._latlng) });
} }
private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings, setFocus = true) { private createMarker(key: string, data: FormattedData, dataSources: FormattedData[], settings: MarkerSettings) {
this.ready$.subscribe(() => { this.ready$.subscribe(() => {
const newMarker = new Marker(this.map, this.convertPosition(data), settings, data, dataSources, () => { }, this.dragMarker); const newMarker = new Marker(this.convertPosition(data), settings, data, dataSources, this.dragMarker);
this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng()), setFocus); this.fitBounds(this.bounds.extend(newMarker.leafletMarker.getLatLng()), settings.draggableMarker && this.markers.size > 2);
this.markers.set(key, newMarker); this.markers.set(key, newMarker);
if (this.options.useClusterMarkers) {
this.markersCluster.addLayer(newMarker.leafletMarker);
}
else {
this.map.addLayer(newMarker.leafletMarker);
}
}); });
} }
@ -242,6 +279,8 @@ export default abstract class LeafletMap {
if (settings.showTooltip) { if (settings.showTooltip) {
marker.updateMarkerTooltip(data); marker.updateMarkerTooltip(data);
} }
if (settings.useClusterMarkers)
this.markersCluster.refreshClusters()
marker.setDataSources(data, dataSources); marker.setDataSources(data, dataSources);
marker.updateMarkerIcon(settings); marker.updateMarkerIcon(settings);
} }
@ -320,7 +359,9 @@ export default abstract class LeafletMap {
updatePolygon(key: string, data: LatLngTuple[], dataSources: DatasourceData[], settings: PolygonSettings) { updatePolygon(key: string, data: LatLngTuple[], dataSources: DatasourceData[], settings: PolygonSettings) {
this.ready$.subscribe(() => { this.ready$.subscribe(() => {
this.polygons.get(key).updatePolygon(data, dataSources, settings); const poly = this.polygons.get(key);
poly.updatePolygon(data, dataSources, settings);
this.fitBounds(poly.leafletPoly.getBounds());
}); });
} }
} }

37
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-models.ts

@ -14,7 +14,7 @@
/// limitations under the License. /// limitations under the License.
/// ///
import { LatLngTuple } from 'leaflet'; import { LatLngTuple, LeafletMouseEvent } from 'leaflet';
export type GenericFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string; export type GenericFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string;
export type MarkerImageFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string; export type MarkerImageFunction = (data: FormattedData, dsData: FormattedData[], dsIndex: number) => string;
@ -24,14 +24,15 @@ export type MapSettings = {
draggableMarker: boolean; draggableMarker: boolean;
initCallback?: () => any; initCallback?: () => any;
defaultZoomLevel?: number; defaultZoomLevel?: number;
dontFitMapBounds?: boolean;
disableScrollZooming?: boolean; disableScrollZooming?: boolean;
minZoomLevel?: number; minZoomLevel?: number;
useClusterMarkers: boolean;
latKeyName?: string; latKeyName?: string;
lngKeyName?: string; lngKeyName?: string;
xPosKeyName?: string; xPosKeyName?: string;
yPosKeyName?: string; yPosKeyName?: string;
mapProvider: MapProviders; mapProvider: MapProviders;
mapProviderHere: string;
mapUrl?: string; mapUrl?: string;
mapImageUrl?: string; mapImageUrl?: string;
provider?: MapProviders; provider?: MapProviders;
@ -42,6 +43,13 @@ export type MapSettings = {
gmDefaultMapType?: string; gmDefaultMapType?: string;
useLabelFunction: string; useLabelFunction: string;
icon?: any; icon?: any;
zoomOnClick: boolean,
maxZoom: number,
showCoverageOnHover: boolean,
animate: boolean,
maxClusterRadius: number,
chunkedLoading: boolean,
removeOutsideVisibleBounds: boolean
} }
export enum MapProviders { export enum MapProviders {
@ -54,6 +62,7 @@ export enum MapProviders {
export type MarkerSettings = { export type MarkerSettings = {
tooltipPattern?: any; tooltipPattern?: any;
tooltipAction: { [name: string]: actionsHandler };
icon?: any; icon?: any;
showLabel?: boolean; showLabel?: boolean;
label: string; label: string;
@ -62,19 +71,19 @@ export type MarkerSettings = {
useLabelFunction: boolean; useLabelFunction: boolean;
draggableMarker: boolean; draggableMarker: boolean;
showTooltip?: boolean; showTooltip?: boolean;
useTooltipFunction: boolean;
useColorFunction: boolean;
color?: string; color?: string;
autocloseTooltip: boolean; autocloseTooltip: boolean;
displayTooltipAction: string; showTooltipAction: string;
useClusterMarkers: boolean;
currentImage?: string; currentImage?: string;
useMarkerImageFunction?: boolean; useMarkerImageFunction?: boolean;
markerImages?: string[]; markerImages?: string[];
useMarkerImage: boolean;
markerImageSize: number; markerImageSize: number;
fitMapBounds: boolean; fitMapBounds: boolean;
markerImage: { markerImage: string;
length: number markerClick: { [name: string]: actionsHandler };
}
colorFunction: GenericFunction; colorFunction: GenericFunction;
tooltipFunction: GenericFunction; tooltipFunction: GenericFunction;
labelFunction: GenericFunction; labelFunction: GenericFunction;
@ -98,16 +107,18 @@ export type PolygonSettings = {
polygonStrokeColor: string; polygonStrokeColor: string;
polygonColor: string; polygonColor: string;
autocloseTooltip: boolean; autocloseTooltip: boolean;
displayTooltipAction: string; showTooltipAction: string;
tooltipAction: object;
polygonClick: { [name: string]: actionsHandler };
polygonColorFunction?: GenericFunction; polygonColorFunction?: GenericFunction;
} }
export type PolylineSettings = { export type PolylineSettings = {
usePolylineDecorator: any; usePolylineDecorator: any;
autocloseTooltip: boolean; autocloseTooltip: boolean;
displayTooltipAction: string; showTooltipAction: string;
useColorFunction: any; useColorFunction: any;
tooltipAction: { [name: string]: actionsHandler };
color: string; color: string;
useStrokeOpacityFunction: any; useStrokeOpacityFunction: any;
strokeOpacity: number; strokeOpacity: number;
@ -131,4 +142,6 @@ export interface HistorySelectSettings {
buttonColor: string; buttonColor: string;
} }
export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings; export type actionsHandler = ($event: Event | LeafletMouseEvent) => void;
export type UnitedMapSettings = MapSettings & PolygonSettings & MarkerSettings & PolylineSettings;

5
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget.interface.ts

@ -15,6 +15,7 @@
/// ///
import { JsonSettingsSchema } from '@shared/models/widget.models'; import { JsonSettingsSchema } from '@shared/models/widget.models';
import { MapProviders } from './map-models';
export interface MapWidgetInterface { export interface MapWidgetInterface {
resize(), resize(),
@ -24,8 +25,8 @@ export interface MapWidgetInterface {
} }
export interface MapWidgetStaticInterface { export interface MapWidgetStaticInterface {
settingsSchema(mapProvider?, drawRoutes?): JsonSettingsSchema; settingsSchema(mapProvider?: MapProviders, drawRoutes?: boolean): JsonSettingsSchema;
getProvidersSchema():JsonSettingsSchema getProvidersSchema(mapProvider?: MapProviders): JsonSettingsSchema
dataKeySettingsSchema(): object; dataKeySettingsSchema(): object;
actionSources(): object; actionSources(): object;
} }

78
ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts

@ -26,20 +26,23 @@ import {
markerClusteringSettingsSchema, markerClusteringSettingsSchema,
markerClusteringSettingsSchemaLeaflet, markerClusteringSettingsSchemaLeaflet,
hereMapSettingsSchema, hereMapSettingsSchema,
mapProviderSchema mapProviderSchema,
mapPolygonSchema
} from './schemes'; } from './schemes';
import { MapWidgetStaticInterface, MapWidgetInterface } from './map-widget.interface'; import { MapWidgetStaticInterface, MapWidgetInterface } from './map-widget.interface';
import { OpenStreetMap, TencentMap, GoogleMap, HEREMap, ImageMap } from './providers'; import { OpenStreetMap, TencentMap, GoogleMap, HEREMap, ImageMap } from './providers';
import { parseFunction, parseArray, parseData } from '@core/utils'; import { parseFunction, parseArray, parseData, safeExecute, parseWithTranslation } from '@core/utils';
import { initSchema, addToSchema, mergeSchemes, addCondition, addGroupInfo } from '@core/schema-utils'; import { initSchema, addToSchema, mergeSchemes, addCondition, addGroupInfo } from '@core/schema-utils';
import { forkJoin } from 'rxjs'; import { forkJoin } from 'rxjs';
import { WidgetContext } from '@app/modules/home/models/widget-component.models'; import { WidgetContext } from '@app/modules/home/models/widget-component.models';
import { getDefCenterPosition } from './maps-utils'; import { getDefCenterPosition } from './maps-utils';
import { JsonSettingsSchema } from '@shared/models/widget.models'; import { JsonSettingsSchema, WidgetActionDescriptor } from '@shared/models/widget.models';
import { EntityId } from '@shared/models/id/entity-id'; import { EntityId } from '@shared/models/id/entity-id';
import { AttributeScope } from '@shared/models/telemetry/telemetry.models'; import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { AttributeService } from '@core/http/attribute.service'; import { AttributeService } from '@core/http/attribute.service';
import { Type } from '@angular/core'; import { Type } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { UtilsService } from '@app/core/public-api';
// @dynamic // @dynamic
export class MapWidgetController implements MapWidgetInterface { export class MapWidgetController implements MapWidgetInterface {
@ -55,11 +58,16 @@ export class MapWidgetController implements MapWidgetInterface {
$element = ctx.$container[0]; $element = ctx.$container[0];
} }
this.settings = this.initSettings(ctx.settings); this.settings = this.initSettings(ctx.settings);
this.settings.tooltipAction = this.getDescriptors('tooltipAction');
this.settings.markerClick = this.getDescriptors('markerClick');
this.settings.polygonClick = this.getDescriptors('polygonClick');
// this.settings.
const MapClass = providerSets[this.provider]?.MapClass; const MapClass = providerSets[this.provider]?.MapClass;
if (!MapClass) { if (!MapClass) {
return; return;
} }
parseWithTranslation.setTranslate(this.translate);
this.map = new MapClass($element, this.settings); this.map = new MapClass($element, this.settings);
this.map.saveMarkerLocation = this.setMarkerLocation; this.map.saveMarkerLocation = this.setMarkerLocation;
} }
@ -74,7 +82,8 @@ export class MapWidgetController implements MapWidgetInterface {
return {}; return {};
} }
public static getProvidersSchema() { public static getProvidersSchema(mapProvider: MapProviders) {
mapProviderSchema.schema.properties.provider.default = mapProvider;
return mergeSchemes([mapProviderSchema, return mergeSchemes([mapProviderSchema,
...Object.values(providerSets)?.map( ...Object.values(providerSets)?.map(
(setting: IProvider) => addCondition(setting?.schema, `model.provider === '${setting.name}'`))]); (setting: IProvider) => addCondition(setting?.schema, `model.provider === '${setting.name}'`))]);
@ -82,19 +91,20 @@ export class MapWidgetController implements MapWidgetInterface {
public static settingsSchema(mapProvider: MapProviders, drawRoutes: boolean): JsonSettingsSchema { public static settingsSchema(mapProvider: MapProviders, drawRoutes: boolean): JsonSettingsSchema {
const schema = initSchema(); const schema = initSchema();
addToSchema(schema, this.getProvidersSchema()); addToSchema(schema, this.getProvidersSchema(mapProvider));
addGroupInfo(schema, 'Map Provider Settings'); if(mapProvider!=='image-map'){
addToSchema(schema, commonMapSettingsSchema); addGroupInfo(schema, 'Map Provider Settings');
addToSchema(schema, mergeSchemes([commonMapSettingsSchema, addCondition(mapPolygonSchema, 'model.showPolygon === true')]));
addGroupInfo(schema, 'Common Map Settings'); addGroupInfo(schema, 'Common Map Settings');
if (drawRoutes) { if (drawRoutes) {
addToSchema(schema, routeMapSettingsSchema); addToSchema(schema, routeMapSettingsSchema);
addGroupInfo(schema, 'Route Map Settings'); addGroupInfo(schema, 'Route Map Settings');
} else if (mapProvider !== 'image-map') { } else if (mapProvider !== 'image-map') {
const clusteringSchema = mergeSchemes([markerClusteringSettingsSchemaLeaflet, markerClusteringSettingsSchema]) const clusteringSchema = mergeSchemes([markerClusteringSettingsSchema,
addCondition(markerClusteringSettingsSchemaLeaflet, `model.useClusterMarkers === true`)])
addToSchema(schema, clusteringSchema); addToSchema(schema, clusteringSchema);
addGroupInfo(schema, 'Markers Clustering Settings'); addGroupInfo(schema, 'Markers Clustering Settings');
} }}
return schema; return schema;
} }
@ -115,9 +125,35 @@ export class MapWidgetController implements MapWidgetInterface {
}; };
} }
translate = (key: string, defaultTranslation?: string):string => {
return (this.ctx.$injector.get(UtilsService).customTranslation(key, defaultTranslation || key)
|| this.ctx.$injector.get(TranslateService).instant(key));
}
getDescriptors(name: string): { [name: string]: ($event: Event) => void } {
const descriptors = this.ctx.actionsApi.getActionDescriptors(name);
const actions = {};
descriptors.forEach(descriptor => {
actions[descriptor.name] = ($event: Event) => this.onCustomAction(descriptor, $event);
}, actions);
return actions;
}
onInit() { onInit() {
} }
private onCustomAction(descriptor: WidgetActionDescriptor, $event: any) {
if ($event & $event.stopPropagation) {
$event?.stopPropagation();
}
// safeExecute(parseFunction(descriptor.customFunction, ['$event', 'widgetContext']), [$event, this.ctx])
const entityInfo = this.ctx.actionsApi.getActiveEntityInfo();
const entityId = entityInfo ? entityInfo.entityId : null;
const entityName = entityInfo ? entityInfo.entityName : null;
const entityLabel = entityInfo ? entityInfo.entityLabel : null;
this.ctx.actionsApi.handleWidgetAction($event, descriptor, entityId, entityName, null, entityLabel);
}
setMarkerLocation = (e) => { setMarkerLocation = (e) => {
const attributeService = this.ctx.$injector.get(AttributeService); const attributeService = this.ctx.$injector.get(AttributeService);
forkJoin( forkJoin(
@ -136,13 +172,17 @@ export class MapWidgetController implements MapWidgetInterface {
}] }]
); );
})).subscribe(res => { })).subscribe(res => {
console.log('MapWidgetController -> setMarkerLocation -> res', res)
}); });
} }
initSettings(settings: UnitedMapSettings): UnitedMapSettings { initSettings(settings: UnitedMapSettings): UnitedMapSettings {
const functionParams = ['data', 'dsData', 'dsIndex']; const functionParams = ['data', 'dsData', 'dsIndex'];
this.provider = settings.provider || this.mapProvider; this.provider = settings.provider || this.mapProvider;
if (!settings.mapProviderHere) {
if (settings.mapProvider && hereProviders.includes(settings.mapProvider))
settings.mapProviderHere = settings.mapProvider
else settings.mapProviderHere = hereProviders[0];
}
const customOptions = { const customOptions = {
provider: this.provider, provider: this.provider,
mapUrl: settings?.mapImageUrl, mapUrl: settings?.mapImageUrl,
@ -156,7 +196,7 @@ export class MapWidgetController implements MapWidgetInterface {
'<b>${entityName}</b><br/><br/><b>Latitude:</b> ${' + '<b>${entityName}</b><br/><br/><b>Latitude:</b> ${' +
settings.latKeyName + ':7}<br/><b>Longitude:</b> ${' + settings.lngKeyName + ':7}', settings.latKeyName + ':7}<br/><b>Longitude:</b> ${' + settings.lngKeyName + ':7}',
defaultCenterPosition: getDefCenterPosition(settings?.defaultCenterPosition), defaultCenterPosition: getDefCenterPosition(settings?.defaultCenterPosition),
currentImage: (settings.useMarkerImage && settings.markerImage?.length) ? { currentImage: (settings.markerImage?.length) ? {
url: settings.markerImage, url: settings.markerImage,
size: settings.markerImageSize || 34 size: settings.markerImageSize || 34
} : null } : null
@ -194,7 +234,7 @@ interface IProvider {
name: string name: string
} }
export const providerSets: {[key: string]: IProvider} = { export const providerSets: { [key: string]: IProvider } = {
'openstreet-map': { 'openstreet-map': {
MapClass: OpenStreetMap, MapClass: OpenStreetMap,
schema: openstreetMapSettingsSchema, schema: openstreetMapSettingsSchema,
@ -236,7 +276,7 @@ export const defaultSettings: any = {
useDefaultCenterPosition: false, useDefaultCenterPosition: false,
showTooltipAction: 'click', showTooltipAction: 'click',
autocloseTooltip: false, autocloseTooltip: false,
showPolygon: true, showPolygon: false,
labelColor: '#000000', labelColor: '#000000',
color: '#FE7569', color: '#FE7569',
polygonColor: '#0000ff', polygonColor: '#0000ff',
@ -250,10 +290,16 @@ export const defaultSettings: any = {
strokeOpacity: 1.0, strokeOpacity: 1.0,
initCallback: () => { }, initCallback: () => { },
defaultZoomLevel: 8, defaultZoomLevel: 8,
dontFitMapBounds: false,
disableScrollZooming: false, disableScrollZooming: false,
minZoomLevel: 16, minZoomLevel: 16,
credentials: '', credentials: '',
markerClusteringSetting: null, markerClusteringSetting: null,
draggableMarker: false draggableMarker: false,
fitMapBounds: true
}; };
export const hereProviders = [
'HERE.normalDay',
'HERE.normalNight',
'HERE.hybridDay',
'HERE.terrainDay']

25
ui-ngx/src/app/modules/home/components/widget/lib/maps/maps-utils.ts

@ -17,20 +17,32 @@
import L from 'leaflet'; import L from 'leaflet';
import _ from 'lodash'; import _ from 'lodash';
import { MarkerSettings, PolylineSettings, PolygonSettings } from './map-models'; import { MarkerSettings, PolylineSettings, PolygonSettings } from './map-models';
import { parseWithTranslation } from '@app/core/utils';
export function createTooltip(target: L.Layer, settings: MarkerSettings | PolylineSettings | PolygonSettings): L.Popup { export function createTooltip(target: L.Layer,
settings: MarkerSettings | PolylineSettings | PolygonSettings,
content?: string | HTMLElement): L.Popup {
const popup = L.popup(); const popup = L.popup();
popup.setContent(''); popup.setContent(content);
target.bindPopup(popup, { autoClose: settings.autocloseTooltip, closeOnClick: false }); target.bindPopup(popup, { autoClose: settings.autocloseTooltip, closeOnClick: false });
if (settings.displayTooltipAction === 'hover') { if (settings.showTooltipAction === 'hover') {
target.off('click'); target.off('click');
target.on('mouseover', function () { target.on('mouseover', function () {
this.openPopup(); target.openPopup();
}); });
target.on('mouseout', function () { target.on('mouseout', function () {
this.closePopup(); target.closePopup();
}); });
} }
target.on('popupopen', () => {
const actions = document.getElementsByClassName('tb-custom-action');
Array.from(actions).forEach(
(element: HTMLElement) => {
if (element && settings.tooltipAction[element.id]) {
element.addEventListener('click', settings.tooltipAction[element.id])
}
});
});
return popup; return popup;
} }
@ -51,5 +63,4 @@ export function getDefCenterPosition(position) {
if (typeof (position) === 'object') if (typeof (position) === 'object')
return position; return position;
return [0, 0]; return [0, 0];
} }

4
ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.scss

@ -29,3 +29,7 @@
background: none; background: none;
box-shadow: none; box-shadow: none;
} }
.leaflet-container{
background-color: white;
}

49
ui-ngx/src/app/modules/home/components/widget/lib/maps/markers.ts

@ -14,13 +14,13 @@
/// limitations under the License. /// limitations under the License.
/// ///
import L from 'leaflet'; import { aspectCache, parseWithTranslation, safeExecute } from '@app/core/utils';
import { MarkerSettings, FormattedData } from './map-models'; import L, { LeafletMouseEvent } from 'leaflet';
import { aspectCache, safeExecute, parseTemplate } from '@app/core/utils'; import { FormattedData, MarkerSettings } from './map-models';
import { createTooltip } from './maps-utils'; import { createTooltip } from './maps-utils';
import tinycolor from 'tinycolor2';
export class Marker { export class Marker {
leafletMarker: L.Marker; leafletMarker: L.Marker;
tooltipOffset: [number, number]; tooltipOffset: [number, number];
tooltip: L.Popup; tooltip: L.Popup;
@ -28,8 +28,8 @@ export class Marker {
data: FormattedData; data: FormattedData;
dataSources: FormattedData[]; dataSources: FormattedData[];
constructor(private map: L.Map, location: L.LatLngExpression, public settings: MarkerSettings, constructor(location: L.LatLngExpression, public settings: MarkerSettings,
data?, dataSources?, onClickListener?, onDragendListener?) { data?: FormattedData, dataSources?, onDragendListener?) {
this.setDataSources(data, dataSources); this.setDataSources(data, dataSources);
this.leafletMarker = L.marker(location, { this.leafletMarker = L.marker(location, {
draggable: settings.draggableMarker draggable: settings.draggableMarker
@ -39,16 +39,21 @@ export class Marker {
this.leafletMarker.setIcon(iconInfo.icon); this.leafletMarker.setIcon(iconInfo.icon);
this.tooltipOffset = [0, -iconInfo.size[1] + 10]; this.tooltipOffset = [0, -iconInfo.size[1] + 10];
this.updateMarkerLabel(settings); this.updateMarkerLabel(settings);
this.leafletMarker.addTo(map);
}); });
if (settings.showTooltip) { if (settings.showTooltip) {
this.tooltip = createTooltip(this.leafletMarker, settings); this.tooltip = createTooltip(this.leafletMarker, settings);
this.tooltip.setContent(parseTemplate(this.settings.tooltipPattern, data)); this.updateMarkerTooltip(data);
} }
if (onClickListener) { if (this.settings.markerClick) {
this.leafletMarker.on('click', onClickListener); this.leafletMarker.on('click', (event: LeafletMouseEvent) => {
for (const action in this.settings.markerClick) {
if (typeof (this.settings.markerClick[action]) === 'function') {
this.settings.markerClick[action](event);
}
}
});
} }
if (onDragendListener) { if (onDragendListener) {
@ -62,7 +67,9 @@ export class Marker {
} }
updateMarkerTooltip(data: FormattedData) { updateMarkerTooltip(data: FormattedData) {
this.tooltip.setContent(parseTemplate(this.settings.tooltipPattern, data)); const pattern = this.settings.useTooltipFunction ?
safeExecute(this.settings.tooltipFunction, [this.data, this.dataSources, this.data.dsIndex]) : this.settings.tooltipPattern;
this.tooltip.setContent(parseWithTranslation.parseTemplate(pattern, data, true));
} }
updateMarkerPosition(position: L.LatLngExpression) { updateMarkerPosition(position: L.LatLngExpression) {
@ -71,13 +78,10 @@ export class Marker {
updateMarkerLabel(settings: MarkerSettings) { updateMarkerLabel(settings: MarkerSettings) {
this.leafletMarker.unbindTooltip(); this.leafletMarker.unbindTooltip();
if (settings.showLabel) { if (settings.showLabel) {
if (settings.useLabelFunction) { const pattern = settings.useLabelFunction ?
settings.labelText = parseTemplate( safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex]) : settings.label;
safeExecute(settings.labelFunction, [this.data, this.dataSources, this.data.dsIndex]), this.data) settings.labelText = parseWithTranslation.parseTemplate(pattern, this.data, true);
}
else settings.labelText = parseTemplate(settings.label, this.data);
this.leafletMarker.bindTooltip(`<div style="color: ${settings.labelColor};"><b>${settings.labelText}</b></div>`, this.leafletMarker.bindTooltip(`<div style="color: ${settings.labelColor};"><b>${settings.labelText}</b></div>`,
{ className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.tooltipOffset }); { className: 'tb-marker-label', permanent: true, direction: 'top', offset: this.tooltipOffset });
} }
@ -98,7 +102,6 @@ export class Marker {
} }
createMarkerIcon(onMarkerIconReady) { createMarkerIcon(onMarkerIconReady) {
if (this.settings.icon) { if (this.settings.icon) {
onMarkerIconReady({ onMarkerIconReady({
size: [30, 30], size: [30, 30],
@ -106,11 +109,11 @@ export class Marker {
}); });
return; return;
} }
const currentImage = this.settings.useMarkerImageFunction ? const currentImage = this.settings.useMarkerImageFunction ?
safeExecute(this.settings.markerImageFunction, safeExecute(this.settings.markerImageFunction,
[this.data, this.settings.markerImages, this.dataSources, this.data.dsIndex]) : this.settings.currentImage; [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) { if (currentImage && currentImage.url) {
aspectCache(currentImage.url).subscribe( aspectCache(currentImage.url).subscribe(
@ -137,19 +140,19 @@ export class Marker {
}; };
onMarkerIconReady(iconInfo); onMarkerIconReady(iconInfo);
} else { } else {
this.createDefaultMarkerIcon(this.settings.color, onMarkerIconReady); this.createDefaultMarkerIcon(currentColor, onMarkerIconReady);
} }
} }
); );
} else { } else {
this.createDefaultMarkerIcon(this.settings.color, onMarkerIconReady); this.createDefaultMarkerIcon(currentColor, onMarkerIconReady);
} }
} }
createDefaultMarkerIcon(color, onMarkerIconReady) { createDefaultMarkerIcon(color, onMarkerIconReady) {
const pinColor = color.substr(1); const pinColor = color.substr(1);
const icon = L.icon({ const icon = L.icon({
iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + pinColor, iconUrl: 'https://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|' + color,
iconSize: [21, 34], iconSize: [21, 34],
iconAnchor: [10, 34], iconAnchor: [10, 34],
popupAnchor: [0, -34], popupAnchor: [0, -34],

7
ui-ngx/src/app/modules/home/components/widget/lib/maps/providers/here-map.ts

@ -20,14 +20,9 @@ import { MapSettings, UnitedMapSettings } from '../map-models';
export class HEREMap extends LeafletMap { export class HEREMap extends LeafletMap {
constructor($container, options: UnitedMapSettings) { constructor($container, options: UnitedMapSettings) {
const defaultCredentials =
{
app_id: 'AhM6TzD9ThyK78CT3ptx',
app_code: 'p6NPiITB3Vv0GMUFnkLOOg'
}
super($container, options); super($container, options);
const map = L.map($container).setView(options?.defaultCenterPosition, options?.defaultZoomLevel); const map = L.map($container).setView(options?.defaultCenterPosition, options?.defaultZoomLevel);
const tileLayer = (L.tileLayer as any).provider(options.mapProvider || 'HERE.normalDay', options.credentials || defaultCredentials); const tileLayer = (L.tileLayer as any).provider(options.mapProviderHere || 'HERE.normalDay', options.credentials);
tileLayer.addTo(map); tileLayer.addTo(map);
super.setMap(map); super.setMap(map);
super.initSettings(options); super.initSettings(options);

153
ui-ngx/src/app/modules/home/components/widget/lib/maps/schemes.ts

@ -110,7 +110,7 @@ export const hereMapSettingsSchema =
title: 'HERE Map Configuration', title: 'HERE Map Configuration',
type: 'object', type: 'object',
properties: { properties: {
mapProvider: { mapProviderHere: {
title: 'Map layer', title: 'Map layer',
type: 'string', type: 'string',
default: 'HERE.normalDay' default: 'HERE.normalDay'
@ -120,11 +120,13 @@ export const hereMapSettingsSchema =
properties: { properties: {
app_id: { app_id: {
title: 'HERE app id', title: 'HERE app id',
type: 'string' type: 'string',
default: 'AhM6TzD9ThyK78CT3ptx'
}, },
app_code: { app_code: {
title: 'HERE app code', title: 'HERE app code',
type: 'string' type: 'string',
default: 'p6NPiITB3Vv0GMUFnkLOOg'
} }
}, },
required: ['app_id', 'app_code'] required: ['app_id', 'app_code']
@ -134,7 +136,7 @@ export const hereMapSettingsSchema =
}, },
form: [ form: [
{ {
key: 'mapProvider', key: 'mapProviderHere',
type: 'rc-select', type: 'rc-select',
multiple: false, multiple: false,
items: [ items: [
@ -339,43 +341,6 @@ export const commonMapSettingsSchema =
type: 'boolean', type: 'boolean',
default: false default: false
}, },
polygonKeyName: {
title: 'Polygon key name',
type: 'string',
default: 'coordinates'
},
polygonColor: {
title: 'Polygon color',
type: 'string'
},
polygonOpacity: {
title: 'Polygon opacity',
type: 'number',
default: 0.5
},
polygonStrokeColor: {
title: 'Stroke color',
type: 'string'
},
polygonStrokeOpacity: {
title: 'Stroke opacity',
type: 'number',
default: 1
},
polygonStrokeWeight: {
title: 'Stroke 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'
},
markerImage: { markerImage: {
title: 'Custom marker image', title: 'Custom marker image',
type: 'string' type: 'string'
@ -455,20 +420,6 @@ export const commonMapSettingsSchema =
{ {
key: 'colorFunction', key: 'colorFunction',
type: 'javascript' type: 'javascript'
}, 'showPolygon', 'polygonKeyName',
{
key: 'polygonColor',
type: 'color'
},
'polygonOpacity',
{
key: 'polygonStrokeColor',
type: 'color'
},
'polygonStrokeOpacity', 'polygonStrokeWeight', 'usePolygonColorFunction',
{
key: 'polygonColorFunction',
type: 'javascript'
}, },
{ {
key: 'markerImage', key: 'markerImage',
@ -488,7 +439,73 @@ export const commonMapSettingsSchema =
type: 'image' type: 'image'
} }
] ]
} },
'showPolygon',
]
};
export const mapPolygonSchema =
{
schema: {
title: 'Map Polygon Configuration',
type: 'object',
properties: {
polygonKeyName: {
title: 'Polygon key name',
type: 'string',
default: 'coordinates'
},
polygonColor: {
title: 'Polygon color',
type: 'string'
},
polygonOpacity: {
title: 'Polygon opacity',
type: 'number',
default: 0.5
},
polygonStrokeColor: {
title: 'Stroke color',
type: 'string'
},
polygonStrokeOpacity: {
title: 'Stroke opacity',
type: 'number',
default: 1
},
polygonStrokeWeight: {
title: 'Stroke 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'
},
},
required: []
},
form: [
'polygonKeyName',
{
key: 'polygonColor',
type: 'color'
},
'polygonOpacity',
{
key: 'polygonStrokeColor',
type: 'color'
},
'polygonStrokeOpacity', 'polygonStrokeWeight', 'usePolygonColorFunction',
{
key: 'polygonColorFunction',
type: 'javascript'
},
] ]
}; };
@ -527,23 +544,12 @@ export const markerClusteringSettingsSchema =
title: 'Use map markers clustering', title: 'Use map markers clustering',
type: 'boolean', type: 'boolean',
default: false default: false
},
zoomOnClick: {
title: 'Zoom when clicking on a cluster',
type: 'boolean',
default: true
},
maxZoom: {
title: 'The maximum zoom level when a marker can be part of a cluster (0 - 18)',
type: 'number'
} }
}, },
required: [] required: []
}, },
form: [ form: [
'useClusterMarkers', 'useClusterMarkers',
'zoomOnClick',
'maxZoom'
] ]
}; };
@ -571,12 +577,23 @@ export const markerClusteringSettingsSchemaGoogle =
] ]
}; };
export const markerClusteringSettingsSchemaLeaflet = export const markerClusteringSettingsSchemaLeaflet =
{ {
schema: { schema: {
title: 'Markers Clustering Configuration Leaflet', title: 'Markers Clustering Configuration Leaflet',
type: 'object', type: 'object',
properties: { properties: {
zoomOnClick: {
title: 'Zoom when clicking on a cluster',
type: 'boolean',
default: true
},
maxZoom: {
title: 'The maximum zoom level when a marker can be part of a cluster (0 - 18)',
type: 'number'
},
showCoverageOnHover: { showCoverageOnHover: {
title: 'Show the bounds of markers when mouse over a cluster', title: 'Show the bounds of markers when mouse over a cluster',
type: 'boolean', type: 'boolean',
@ -606,6 +623,8 @@ export const markerClusteringSettingsSchemaLeaflet =
required: [] required: []
}, },
form: [ form: [
'zoomOnClick',
'maxZoom',
'showCoverageOnHover', 'showCoverageOnHover',
'animate', 'animate',
'maxClusterRadius', 'maxClusterRadius',

10
ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.html

@ -17,17 +17,17 @@
--> -->
<div class="trip-animation-widget"> <div class="trip-animation-widget">
<div class="trip-animation-label-container" *ngIf="settings.showLabel"> <div class="trip-animation-label-container" *ngIf="settings.showLabel">
{{settings.label | tbParseTemplate: activeTrip}} {{label }}
</div> </div>
<div class="trip-animation-container" layout="column"> <div class="trip-animation-container" fxLayout="column">
<div class="map" #map></div> <div class="map" #map></div>
<div class="trip-animation-info-panel" layout="row"> <div class="trip-animation-info-panel" fxLayout="row">
<button class="tooltip-button" mat-mini-fab color="primary" aria-label="tooltip" <button class="tooltip-button" mat-mini-fab color="primary" aria-label="tooltip"
*ngIf="settings.showTooltip" (click)="showHideTooltip()"> *ngIf="settings.showTooltip" (click)="visibleTooltip = !visibleTooltip">
<mat-icon>info_outline</mat-icon> <mat-icon>info_outline</mat-icon>
</button> </button>
</div> </div>
<div class="trip-animation-tooltip md-whiteframe-z4" layout="column" <div class="trip-animation-tooltip md-whiteframe-z4" fxLayout="column"
[ngClass]="{'trip-animation-tooltip-hidden':!visibleTooltip}" [innerHTML]="mainTooltip" [ngClass]="{'trip-animation-tooltip-hidden':!visibleTooltip}" [innerHTML]="mainTooltip"
[ngStyle]="{'background-color': settings.tooltipColor, 'opacity': settings.tooltipOpacity, 'color': settings.tooltipFontColor}"> [ngStyle]="{'background-color': settings.tooltipColor, 'opacity': settings.tooltipOpacity, 'color': settings.tooltipFontColor}">
</div> </div>

8
ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.scss

@ -74,10 +74,10 @@
.trip-animation-tooltip { .trip-animation-tooltip {
position: absolute; position: absolute;
top: 38px; top: 30px;
right: 0; right: 0;
z-index: 400; z-index: 1000;
padding: 10px; padding: 5px;
background-color: #fff; background-color: #fff;
transition: 0.3s ease-in-out; transition: 0.3s ease-in-out;
@ -86,4 +86,4 @@
} }
} }
} }
} }

42
ui-ngx/src/app/modules/home/components/widget/trip-animation/trip-animation.component.ts

@ -22,13 +22,14 @@ import { interpolateOnPointSegment } from 'leaflet-geometryutil';
import { Component, OnInit, Input, ViewChild, AfterViewInit, ChangeDetectorRef, SecurityContext } from '@angular/core'; import { Component, OnInit, Input, ViewChild, AfterViewInit, ChangeDetectorRef, SecurityContext } from '@angular/core';
import { MapWidgetController, TbMapWidgetV2 } from '../lib/maps/map-widget2'; import { MapWidgetController, TbMapWidgetV2 } from '../lib/maps/map-widget2';
import { MapProviders } from '../lib/maps/map-models'; import { MapProviders } from '../lib/maps/map-models';
import { parseArray, parseTemplate, safeExecute } from '@app/core/utils'; import { parseArray, parseWithTranslation, safeExecute, parseTemplate } from '@app/core/utils';
import { initSchema, addToSchema, addGroupInfo } from '@app/core/schema-utils'; import { initSchema, addToSchema, addGroupInfo } from '@app/core/schema-utils';
import { tripAnimationSchema } from '../lib/maps/schemes'; import { tripAnimationSchema } from '../lib/maps/schemes';
import { DomSanitizer } from '@angular/platform-browser'; import { DomSanitizer } from '@angular/platform-browser';
import { WidgetContext } from '@app/modules/home/models/widget-component.models'; import { WidgetContext } from '@app/modules/home/models/widget-component.models';
import { getRatio, findAngle } from '../lib/maps/maps-utils'; import { getRatio, findAngle } from '../lib/maps/maps-utils';
import { JsonSettingsSchema, WidgetConfig } from '@shared/models/widget.models'; import { JsonSettingsSchema, WidgetConfig } from '@shared/models/widget.models';
import moment from 'moment';
@Component({ @Component({
@ -55,6 +56,9 @@ export class TripAnimationComponent implements OnInit, AfterViewInit {
mainTooltip = ''; mainTooltip = '';
visibleTooltip = false; visibleTooltip = false;
activeTrip; activeTrip;
label;
minTime;
maxTime;
static getSettingsSchema(): JsonSettingsSchema { static getSettingsSchema(): JsonSettingsSchema {
const schema = initSchema(); const schema = initSchema();
@ -90,48 +94,58 @@ export class TripAnimationComponent implements OnInit, AfterViewInit {
ngAfterViewInit() { ngAfterViewInit() {
const ctxCopy: WidgetContext = _.cloneDeep(this.ctx); const ctxCopy: WidgetContext = _.cloneDeep(this.ctx);
ctxCopy.settings.showLabel = false;
ctxCopy.settings.showTooltip = false;
this.mapWidget = new MapWidgetController(MapProviders.openstreet, false, ctxCopy, this.mapContainer.nativeElement); this.mapWidget = new MapWidgetController(MapProviders.openstreet, false, ctxCopy, this.mapContainer.nativeElement);
} }
timeUpdated(time: number) { timeUpdated(time: number) {
const currentPosition = this.interpolatedData.map(dataSource => dataSource[time]); const currentPosition = this.interpolatedData.map(dataSource => dataSource[time]);
this.activeTrip = currentPosition[0]; this.activeTrip = currentPosition[0];
if (this.settings.showPolygon) { this.minTime = moment(this.historicalData[0][this.historicalData.length - 1]?.time).format('YYYY-MM-DD HH:mm:ss')
this.mapWidget.map.updatePolygons(this.interpolatedData); this.maxTime = moment(this.historicalData[0][0]?.time).format('YYYY-MM-DD HH:mm:ss')
this.calcLabel();
this.calcTooltip();
if (this.mapWidget) {
if (this.settings.showPolygon) {
this.mapWidget.map.updatePolygons(this.interpolatedData);
}
this.mapWidget.map.updateMarkers(currentPosition);
} }
this.mapWidget.map.updateMarkers(currentPosition);
} }
setActiveTrip() { setActiveTrip() {
} }
calculateIntervals() { calculateIntervals() {
this.historicalData.forEach((dataSource, index) => { this.historicalData.forEach((dataSource, index) => {
this.intervals = []; this.intervals = [];
for (let time = dataSource[0]?.time; time < dataSource[dataSource.length - 1]?.time; time += this.normalizationStep) { for (let time = dataSource[0]?.time; time < dataSource[dataSource.length - 1]?.time; time += this.normalizationStep) {
this.intervals.push(time); this.intervals.push(time);
} }
this.intervals.push(dataSource[dataSource.length - 1]?.time); this.intervals.push(dataSource[dataSource.length - 1]?.time);
this.interpolatedData[index] = this.interpolateArray(dataSource, this.intervals); this.interpolatedData[index] = this.interpolateArray(dataSource, this.intervals);
}); });
} }
showHideTooltip() { calcTooltip() {
const data = { ...this.activeTrip, maxTime: this.maxTime, minTime: this.minTime }
const tooltipText: string = this.settings.useTooltipFunction ? const tooltipText: string = this.settings.useTooltipFunction ?
safeExecute(this.settings.tooolTipFunction, [this.activeTrip, this.historicalData, 0]) safeExecute(this.settings.tooolTipFunction, [data, this.historicalData, 0]) : this.settings.tooltipPattern;
: this.settings.tooltipPattern; this.mainTooltip = this.sanitizer.sanitize(
SecurityContext.HTML, (parseWithTranslation.parseTemplate(tooltipText, data, true)));
}
this.mainTooltip = this.sanitizer.sanitize(SecurityContext.HTML, parseTemplate(tooltipText, this.activeTrip)) calcLabel() {
this.visibleTooltip = !this.visibleTooltip; const data = { ...this.activeTrip, maxTime: this.maxTime, minTime: this.minTime }
const labelText: string = this.settings.useLabelFunction ?
safeExecute(this.settings.labelFunction, [data, this.historicalData, 0]) : this.settings.label;
this.label = (parseWithTranslation.parseTemplate(labelText, data, true));
} }
interpolateArray(originData, interpolatedIntervals) { interpolateArray(originData, interpolatedIntervals) {
const result = {}; const result = {};
for (let i = 1, j = 0; i < originData.length && j < interpolatedIntervals.length;) { for (let i = 1, j = 0; i < originData.length && j < interpolatedIntervals.length;) {
const currentTime = interpolatedIntervals[j]; const currentTime = interpolatedIntervals[j];
while (originData[i].time < currentTime) i++; while (originData[i].time < currentTime) i++;

23
ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.html

@ -16,15 +16,21 @@
--> -->
<div class="trip-animation-control-panel"> <div class="trip-animation-control-panel">
<div> <div fxFlex fxLayout="row" fxFlexAlign="center">
<button mat-icon-button class="mat-icon-button" aria-label="Start" (click)="moveStart()"> <button mat-icon-button class="mat-icon-button" aria-label="Start" (click)="moveStart()">
<mat-icon class="material-icons" [ngStyle]="{'color': settings.buttonColor}">fast_rewind</mat-icon> <mat-icon class="material-icons" [ngStyle]="{'color': settings.buttonColor}">fast_rewind</mat-icon>
</button> </button>
<button mat-icon-button class="mat-icon-button" aria-label="Previous" (click)="movePrev()"> <button mat-icon-button class="mat-icon-button" aria-label="Previous" (click)="movePrev()">
<mat-icon class="material-icons" [ngStyle]="{'color': settings.buttonColor}">skip_previous</mat-icon> <mat-icon class="material-icons" [ngStyle]="{'color': settings.buttonColor}">skip_previous</mat-icon>
</button> </button>
<mat-slider [(ngModel)]="index" [min]="minTimeIndex" [max]="maxTimeIndex" (change)="changeIndex()"> <div fxLayout="column" fxFlex="100">
</mat-slider> <mat-slider [(ngModel)]="index" [min]="minTimeIndex" [max]="maxTimeIndex" (change)="changeIndex()">
</mat-slider>
<div class="panel-timer">
<span *ngIf="this.intervals[this.index]">{{ this.intervals[this.index] | date:'medium'}}</span>
<span *ngIf="!this.intervals[this.index]">{{ "widget.no-data-found" | translate}}</span>
</div>
</div>
<button mat-icon-button class="mat-icon-button" aria-label="Next" (click)="moveNext()"> <button mat-icon-button class="mat-icon-button" aria-label="Next" (click)="moveNext()">
<mat-icon class="material-icons" [ngStyle]="{'color': settings.buttonColor}">skip_next</mat-icon> <mat-icon class="material-icons" [ngStyle]="{'color': settings.buttonColor}">skip_next</mat-icon>
</button> </button>
@ -41,11 +47,8 @@
pause_circle_outline pause_circle_outline
</mat-icon> </mat-icon>
</button> </button>
<mat-select matInput [(ngModel)]="speed" (selectionChange)="reeneble()" class="speed-select" aria-label="Speed selector"> <mat-select matInput [(ngModel)]="speed" (selectionChange)="reeneble()" class="speed-select"
<mat-option [value]="speedValue" flex="1" *ngFor="let speedValue of speeds">{{speedValue}} </mat-option> aria-label="Speed selector">
<mat-option [value]="speedValue" *ngFor="let speedValue of speeds">{{speedValue}} </mat-option>
</mat-select> </mat-select>
</div> </div>
<div class="panel-timer">
<span *ngIf="this.intervals[this.index]">{{ this.intervals[this.index] | date:'medium'}}</span>
<span *ngIf="!this.intervals[this.index]">{{ "widget.no-data-found" | translate}}</span>
</div>

186
ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.scss

@ -13,128 +13,120 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
.trip-animation-widget {
.trip-animation-label-container {
height: 24px;
}
.trip-animation-container {
position: relative; position: relative;
z-index: 1;
flex: 1;
width: 100%; width: 100%;
height: 100%;
font-size: 16px;
line-height: 24px;
.trip-animation-label-container { #trip-animation-map {
height: 24px;
}
.trip-animation-container {
position: relative;
z-index: 1; z-index: 1;
flex: 1;
width: 100%; width: 100%;
height: 100%;
#trip-animation-map { .pointsLayerMarkerIcon {
z-index: 1; border-radius: 50%;
width: 100%;
height: 100%;
.pointsLayerMarkerIcon {
border-radius: 50%;
}
} }
}
.trip-animation-info-panel {
position: absolute;
top: 0;
right: 0;
z-index: 2;
pointer-events: none;
.trip-animation-info-panel { .mat-button {
position: absolute;
top: 0; top: 0;
right: 0; left: 0;
z-index: 2; width: 32px;
pointer-events: none; min-width: 32px;
height: 32px;
.md-button { min-height: 32px;
top: 0; padding: 0 0 2px;
left: 0; margin: 2px;
width: 32px; line-height: 24px;
min-width: 32px;
height: 32px; ng-mat-icon {
min-height: 32px; width: 24px;
padding: 0 0 2px; height: 24px;
margin: 2px;
line-height: 24px; svg {
width: inherit;
ng-md-icon { height: inherit;
width: 24px;
height: 24px;
svg {
width: inherit;
height: inherit;
}
} }
} }
} }
}
.trip-animation-tooltip { .trip-animation-tooltip {
position: absolute; position: absolute;
top: 38px; top: 38px;
right: 0; right: 0;
z-index: 2; z-index: 2;
padding: 10px; padding: 10px;
background-color: #fff; background-color: #fff;
transition: 0.3s ease-in-out; transition: 0.3s ease-in-out;
&-hidden { &-hidden {
transform: translateX(110%); transform: translateX(110%);
}
} }
} }
}
.trip-animation-control-panel { .trip-animation-control-panel {
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
padding-bottom: 16px; padding-bottom: 16px;
padding-left: 10px; padding-left: 10px;
md-slider-container { mat-slider-container {
md-slider { mat-slider {
min-width: 80px; min-width: 80px;
} }
button.md-button.md-icon-button { button.mat-button.mat-icon-button {
width: 44px; width: 44px;
min-width: 44px; min-width: 44px;
height: 44px; height: 44px;
min-height: 44px; min-height: 44px;
margin: 0; margin: 0;
line-height: 28px; line-height: 28px;
md-icon { mat-icon {
width: 28px; width: 28px;
height: 28px; height: 28px;
font-size: 28px; font-size: 28px;
svg { svg {
width: inherit; width: inherit;
height: inherit; height: inherit;
}
} }
} }
md-select {
margin: 0;
}
} }
.panel-timer { mat-select {
max-width: none; margin: 0;
padding-right: 250px;
padding-left: 90px;
margin-top: -20px;
font-size: 12px;
font-weight: 500;
text-align: center;
} }
} }
.panel-timer {
max-width: none;
margin-top: -20px;
font-size: 12px;
font-weight: 500;
text-align: center;
}
} }
.speed-select{ .speed-select {
width: 50px; width: 50px;
margin-left: 20px; margin-left: 10px;
margin-top: 10px;
} }

4
ui-ngx/src/app/shared/components/time/history-selector/history-selector.component.ts

@ -15,8 +15,8 @@
/// ///
import { Component, OnInit, OnChanges, Input, Output, EventEmitter, ChangeDetectorRef } from '@angular/core'; import { Component, OnInit, OnChanges, Input, Output, EventEmitter, ChangeDetectorRef } from '@angular/core';
import { interval, Subscription, Subscriber, SubscriptionLike, Observer } from 'rxjs'; import { interval } from 'rxjs';
import { filter, tap } from 'rxjs/operators'; import { filter } from 'rxjs/operators';
import { HistorySelectSettings } from '@app/modules/home/components/widget/lib/maps/map-models'; import { HistorySelectSettings } from '@app/modules/home/components/widget/lib/maps/map-models';
@Component({ @Component({

4
ui-ngx/src/app/shared/pipe/template.pipe.ts

@ -15,11 +15,11 @@
/// ///
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import { parseTemplate } from '@app/core/utils'; import { parseTemplate, parseWithTranslation } from '@app/core/utils';
@Pipe({ name: 'tbParseTemplate' }) @Pipe({ name: 'tbParseTemplate' })
export class TbTemplatePipe implements PipeTransform { export class TbTemplatePipe implements PipeTransform {
transform(template, data): string { transform(template, data): string {
return parseTemplate(template, data); return parseWithTranslation.parseTemplate(template, data);
} }
} }

Loading…
Cancel
Save