You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@streampipes.apache.org by ri...@apache.org on 2020/03/06 23:31:50 UTC

[incubator-streampipes] branch dev updated: STREAMPIPES-58: Add map widget

This is an automated email from the ASF dual-hosted git repository.

riemer pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/incubator-streampipes.git


The following commit(s) were added to refs/heads/dev by this push:
     new 79a3f73  STREAMPIPES-58: Add map widget
79a3f73 is described below

commit 79a3f7339e5a0ed433b472b491feac0ddedacf99
Author: Dominik Riemer <ri...@fzi.de>
AuthorDate: Sat Mar 7 00:31:37 2020 +0100

    STREAMPIPES-58: Add map widget
---
 ui/angular.json                                    |   7 +-
 ui/package.json                                    |   3 +
 ui/src/app/connect/model/Option.ts                 |   7 +-
 .../model/RuntimeResolvableAnyStaticProperty.ts    |   4 +-
 .../model/RuntimeResolvableOneOfStaticProperty.ts  |   4 +-
 .../app/connect/model/SelectionStaticProperty.ts   |   3 +-
 .../static-property-util.service.ts                |   8 +-
 .../widget/dashboard-widget.component.html         |   6 +
 .../widget/dashboard-widget.component.ts           |   3 +-
 .../widgets/base/base-ngx-charts-widget.ts         |  36 +-----
 .../components/widgets/base/base-widget.ts         |  41 ++++++-
 .../components/widgets/map/map-config.ts           |  47 ++++++++
 .../widgets/map/map-widget.component.css           |  31 ++++++
 .../widgets/map/map-widget.component.html          |  28 +++++
 .../components/widgets/map/map-widget.component.ts | 122 +++++++++++++++++++++
 .../widgets/number/number-widget.component.ts      |   8 +-
 .../widgets/table/table-widget.component.ts        |   8 +-
 ui/src/app/dashboard-v2/dashboard.module.ts        |   6 +-
 .../add-visualization-dialog.component.ts          |   2 +-
 .../dashboard-v2/registry/widget-config-builder.ts |  35 ++++--
 .../app/dashboard-v2/registry/widget-registry.ts   |   4 +-
 ui/src/app/dashboard-v2/sdk/ep-requirements.ts     |   9 ++
 .../sdk/extractor/static-property-extractor.ts     |  11 ++
 ui/src/app/dashboard-v2/sdk/model/vocabulary.ts    |   1 +
 ui/src/scss/main.scss                              |   1 +
 25 files changed, 378 insertions(+), 57 deletions(-)

diff --git a/ui/angular.json b/ui/angular.json
index 8605685..86a210e 100644
--- a/ui/angular.json
+++ b/ui/angular.json
@@ -18,7 +18,12 @@
             "tsConfig": "src/tsconfig.app.json",
             "polyfills": "src/polyfills.ts",
             "assets": [
-              "src/assets"
+              "src/assets",
+              {
+                "glob": "**/*",
+                "input": "node_modules/leaflet/dist/images",
+                "output": "assets/img"
+              }
             ],
             "styles": [
               "src/scss/main.scss"
diff --git a/ui/package.json b/ui/package.json
index 5215d7c..37a251a 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -29,6 +29,7 @@
     "@angular/platform-browser-dynamic": "9.0.1",
     "@angular/router": "9.0.1",
     "@angular/upgrade": "9.0.1",
+    "@asymmetrik/ngx-leaflet": "^6.0.1",
     "@fortawesome/fontawesome-free": "^5.12.1",
     "@ngui/datetime-picker": "0.16.2",
     "@stomp/ng2-stompjs": "^7.2.0",
@@ -79,6 +80,7 @@
     "jsplumb": "2.1.3",
     "jszip": "^3.2.1",
     "konva": "^3.2.4",
+    "leaflet": "^1.6.0",
     "lodash": "3.10.1",
     "ng-dynamic-component": "4.0.3",
     "ng-file-upload": "9.0.13",
@@ -110,6 +112,7 @@
     "@types/angular": "^1.6.43",
     "@types/jasmine": "~2.8.3",
     "@types/jqueryui": "^1.12.7",
+    "@types/leaflet": "^1.5.9",
     "@types/node": "^12.11.1",
     "@types/rx": "^4.1.1",
     "codelyzer": "^5.1.2",
diff --git a/ui/src/app/connect/model/Option.ts b/ui/src/app/connect/model/Option.ts
index ff70873..5042495 100644
--- a/ui/src/app/connect/model/Option.ts
+++ b/ui/src/app/connect/model/Option.ts
@@ -19,9 +19,10 @@
 import {RdfId} from '../../platform-services/tsonld/RdfId';
 import {RdfProperty} from '../../platform-services/tsonld/RdfsProperty';
 import {RdfsClass} from '../../platform-services/tsonld/RdfsClass';
+import {UnnamedStreamPipesEntity} from "./UnnamedStreamPipesEntity";
 
 @RdfsClass('sp:Option')
-export class Option {
+export class Option extends UnnamedStreamPipesEntity {
   @RdfId
   public id: string;
 
@@ -36,4 +37,8 @@ export class Option {
 
   @RdfProperty('sp:isSelected')
   public selected: boolean;
+
+  constructor() {
+    super();
+  }
 }
diff --git a/ui/src/app/connect/model/RuntimeResolvableAnyStaticProperty.ts b/ui/src/app/connect/model/RuntimeResolvableAnyStaticProperty.ts
index 479ec6b..88b27b5 100644
--- a/ui/src/app/connect/model/RuntimeResolvableAnyStaticProperty.ts
+++ b/ui/src/app/connect/model/RuntimeResolvableAnyStaticProperty.ts
@@ -26,7 +26,7 @@ export class RuntimeResolvableAnyStaticProperty extends AnyStaticProperty {
     @RdfProperty('sp:dependsOnStaticProperty')
     public dependsOn: string[] = [];
 
-    constructor(id: string) {
-        super(id);
+    constructor() {
+        super();
     }
 }
\ No newline at end of file
diff --git a/ui/src/app/connect/model/RuntimeResolvableOneOfStaticProperty.ts b/ui/src/app/connect/model/RuntimeResolvableOneOfStaticProperty.ts
index 838e19b..3dbd6b1 100644
--- a/ui/src/app/connect/model/RuntimeResolvableOneOfStaticProperty.ts
+++ b/ui/src/app/connect/model/RuntimeResolvableOneOfStaticProperty.ts
@@ -26,8 +26,8 @@ export class RuntimeResolvableOneOfStaticProperty extends OneOfStaticProperty {
     @RdfProperty('sp:dependsOnStaticProperty')
     public dependsOn: string[] = [];
 
-    constructor(id: string) {
-        super(id);
+    constructor() {
+        super();
     }
 
 
diff --git a/ui/src/app/connect/model/SelectionStaticProperty.ts b/ui/src/app/connect/model/SelectionStaticProperty.ts
index 65e490d..8745836 100644
--- a/ui/src/app/connect/model/SelectionStaticProperty.ts
+++ b/ui/src/app/connect/model/SelectionStaticProperty.ts
@@ -37,8 +37,7 @@ export class SelectionStaticProperty extends StaticProperty {
   @RdfProperty('sp:isHorizontalRendering')
   public horizontalRendering: boolean = true;
 
-  constructor(id: string) {
+  constructor() {
     super();
-    this.id = id;
   }
 }
diff --git a/ui/src/app/connect/static-properties/static-property-util.service.ts b/ui/src/app/connect/static-properties/static-property-util.service.ts
index f34348c..b85fbd9 100644
--- a/ui/src/app/connect/static-properties/static-property-util.service.ts
+++ b/ui/src/app/connect/static-properties/static-property-util.service.ts
@@ -107,9 +107,10 @@ export class StaticPropertyUtilService{
         }
         //SelectionStaticProperty
         else if (val instanceof RuntimeResolvableAnyStaticProperty ||  val instanceof RuntimeResolvableOneOfStaticProperty){
-            val instanceof RuntimeResolvableAnyStaticProperty ? clone = new RuntimeResolvableAnyStaticProperty(id) :
-              clone = new RuntimeResolvableOneOfStaticProperty(id);
+            val instanceof RuntimeResolvableAnyStaticProperty ? clone = new RuntimeResolvableAnyStaticProperty() :
+              clone = new RuntimeResolvableOneOfStaticProperty();
 
+            clone.id = id;
             clone.dependsOn = val.dependsOn;
             clone.value = val.value;
             clone.requiredDomainProperty = val.requiredDomainProperty;
@@ -117,8 +118,9 @@ export class StaticPropertyUtilService{
             clone.horizontalRendering = val.horizontalRendering;
         }
         else if (val instanceof AnyStaticProperty || val instanceof OneOfStaticProperty){
-            val instanceof AnyStaticProperty ? clone = new AnyStaticProperty(id) : clone = new OneOfStaticProperty(id);
+            val instanceof AnyStaticProperty ? clone = new AnyStaticProperty() : clone = new OneOfStaticProperty();
 
+            clone.id = id;
             clone.value = val.value;
             clone.requiredDomainProperty = val.requiredDomainProperty;
             clone.options = val.options.map(option => this.cloneOption(option));
diff --git a/ui/src/app/dashboard-v2/components/widget/dashboard-widget.component.html b/ui/src/app/dashboard-v2/components/widget/dashboard-widget.component.html
index 89d8f01..b667b28 100644
--- a/ui/src/app/dashboard-v2/components/widget/dashboard-widget.component.html
+++ b/ui/src/app/dashboard-v2/components/widget/dashboard-widget.component.html
@@ -64,6 +64,12 @@
                                   [widgetConfig]="configuredWidget"
                                   [widgetDataConfig]="widgetDataConfig" class="h-100"></image-widget>
                 </div>
+                <div *ngIf="widget.widgetType === 'map'" class="h-100 p-0">
+                    <map-widget [gridsterItemComponent]="gridsterItemComponent"
+                                  [gridsterItem]="item" [widget]="widget" [editMode]="editMode"
+                                  [widgetConfig]="configuredWidget"
+                                  [widgetDataConfig]="widgetDataConfig" class="h-100"></map-widget>
+                </div>
             </div>
         </div>
     </div>
diff --git a/ui/src/app/dashboard-v2/components/widget/dashboard-widget.component.ts b/ui/src/app/dashboard-v2/components/widget/dashboard-widget.component.ts
index f2994bb..042f4df 100644
--- a/ui/src/app/dashboard-v2/components/widget/dashboard-widget.component.ts
+++ b/ui/src/app/dashboard-v2/components/widget/dashboard-widget.component.ts
@@ -68,7 +68,8 @@ export class DashboardWidgetComponent implements OnInit {
             height: '500px',
             panelClass: 'custom-dialog-container',
             data: {
-                "widget": this.configuredWidget
+                "widget": this.configuredWidget,
+                "pipeline": this.widgetDataConfig
             }
         });
 
diff --git a/ui/src/app/dashboard-v2/components/widgets/base/base-ngx-charts-widget.ts b/ui/src/app/dashboard-v2/components/widgets/base/base-ngx-charts-widget.ts
index 619bd3d..883576d 100644
--- a/ui/src/app/dashboard-v2/components/widgets/base/base-ngx-charts-widget.ts
+++ b/ui/src/app/dashboard-v2/components/widgets/base/base-ngx-charts-widget.ts
@@ -29,8 +29,8 @@ export abstract class BaseNgxChartsStreamPipesWidget extends BaseStreamPipesWidg
 
     colorScheme: any;
 
-    constructor(rxStompService: RxStompService, protected resizeService: ResizeService) {
-        super(rxStompService);
+    constructor(rxStompService: RxStompService, resizeService: ResizeService) {
+        super(rxStompService, resizeService, true);
     }
 
     ngOnInit() {
@@ -39,36 +39,12 @@ export abstract class BaseNgxChartsStreamPipesWidget extends BaseStreamPipesWidg
         this.view = [this.computeCurrentWidth(this.gridsterItemComponent),
             this.computeCurrentHeight(this.gridsterItemComponent)];
         this.displayChart = true;
-        this.resizeService.resizeSubject.subscribe(info => {
-            this.onResize(info);
-        });
     }
 
-    onResize(info: GridsterInfo) {
-        if (info.gridsterItem.id === this.gridsterItem.id) {
-            setTimeout(() => {
-                this.displayChart = false;
-                this.view = [this.computeCurrentWidth(info.gridsterItemComponent),
-                    this.computeCurrentHeight(info.gridsterItemComponent)];
-                this.displayChart = true;
-            }, 100);
-        }
-    }
-
-    computeCurrentWidth(gridsterItemComponent: GridsterItemComponent): number {
-        return (gridsterItemComponent.width - (BaseNgxChartsStreamPipesWidget.PADDING * 2));
-    }
-
-    computeCurrentHeight(gridsterItemComponent: GridsterItemComponent): number {
-        return (gridsterItemComponent.height - (BaseNgxChartsStreamPipesWidget.PADDING * 2) - this.editModeOffset() - this.titlePanelOffset());
-    }
-
-    editModeOffset(): number {
-        return this.editMode ? BaseNgxChartsStreamPipesWidget.EDIT_HEADER_HEIGHT : 0;
-    }
-
-    titlePanelOffset(): number {
-        return this.hasTitlePanelSettings ? 20 : 0;
+    protected onSizeChanged(width: number, height: number) {
+        this.displayChart = false;
+        this.view = [width, height];
+        this.displayChart = true;
     }
 
 }
\ No newline at end of file
diff --git a/ui/src/app/dashboard-v2/components/widgets/base/base-widget.ts b/ui/src/app/dashboard-v2/components/widgets/base/base-widget.ts
index 340bc3d..7b2e057 100644
--- a/ui/src/app/dashboard-v2/components/widgets/base/base-widget.ts
+++ b/ui/src/app/dashboard-v2/components/widgets/base/base-widget.ts
@@ -26,6 +26,8 @@ import {Subscription} from "rxjs";
 import {GridsterItem, GridsterItemComponent} from "angular-gridster2";
 import {WidgetConfigBuilder} from "../../../registry/widget-config-builder";
 import {VisualizablePipeline} from "../../../../core-model/dashboard/VisualizablePipeline";
+import {ResizeService} from "../../../services/resize.service";
+import {GridsterInfo} from "../../../models/gridster-info.model";
 
 export abstract class BaseStreamPipesWidget implements OnChanges {
 
@@ -53,11 +55,17 @@ export abstract class BaseStreamPipesWidget implements OnChanges {
     defaultPrimaryTextColor: string = "#FFFFFF";
     defaultSecondaryTextColor: string = "#39B54A";
 
-    protected constructor(private rxStompService: RxStompService) {
+
+    protected constructor(private rxStompService: RxStompService,
+                          protected resizeService: ResizeService,
+                          protected adjustPadding: boolean) {
     }
 
     ngOnInit(): void {
         this.prepareConfigExtraction();
+        this.resizeService.resizeSubject.subscribe(info => {
+            this.onResize(info);
+        });
         this.subscription = this.rxStompService.watch("/topic/" +this.widgetDataConfig.topic).subscribe((message: Message) => {
             this.onEvent(JSON.parse(message.body));
         });
@@ -86,13 +94,44 @@ export abstract class BaseStreamPipesWidget implements OnChanges {
         this.subscription.unsubscribe();
     }
 
+    computeCurrentWidth(gridsterItemComponent: GridsterItemComponent): number {
+        return this.adjustPadding ?
+            (gridsterItemComponent.width - (BaseStreamPipesWidget.PADDING * 2)) :
+            gridsterItemComponent.width;
+    }
+
+    computeCurrentHeight(gridsterItemComponent: GridsterItemComponent): number {
+        return this.adjustPadding ?
+            (gridsterItemComponent.height - (BaseStreamPipesWidget.PADDING * 2) - this.editModeOffset() - this.titlePanelOffset()) :
+            gridsterItemComponent.height - this.editModeOffset() - this.titlePanelOffset();
+    }
+
+    editModeOffset(): number {
+        return this.editMode ? BaseStreamPipesWidget.EDIT_HEADER_HEIGHT : 0;
+    }
+
+    titlePanelOffset(): number {
+        return this.hasTitlePanelSettings ? 20 : 0;
+    }
+
     protected abstract extractConfig(extractor: StaticPropertyExtractor);
 
     protected abstract onEvent(event: any);
 
+    protected abstract onSizeChanged(width: number, height: number);
+
     ngOnChanges(changes: SimpleChanges): void {
         if (changes["widgetConfig"]) {
             this.prepareConfigExtraction();
         }
     }
+
+    onResize(info: GridsterInfo) {
+        if (info.gridsterItem.id === this.gridsterItem.id) {
+            setTimeout(() => {
+                this.onSizeChanged(this.computeCurrentWidth(info.gridsterItemComponent),
+                    this.computeCurrentHeight(info.gridsterItemComponent))
+            }, 100);
+        }
+    }
 }
\ No newline at end of file
diff --git a/ui/src/app/dashboard-v2/components/widgets/map/map-config.ts b/ui/src/app/dashboard-v2/components/widgets/map/map-config.ts
new file mode 100644
index 0000000..3545886
--- /dev/null
+++ b/ui/src/app/dashboard-v2/components/widgets/map/map-config.ts
@@ -0,0 +1,47 @@
+/*
+ *   Licensed to the Apache Software Foundation (ASF) under one or more
+ *   contributor license agreements.  See the NOTICE file distributed with
+ *   this work for additional information regarding copyright ownership.
+ *   The ASF licenses this file to You under the Apache License, Version 2.0
+ *   (the "License"); you may not use this file except in compliance with
+ *   the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ */
+
+import {WidgetConfigBuilder} from "../../../registry/widget-config-builder";
+import {SchemaRequirementsBuilder} from "../../../sdk/schema-requirements-builder";
+import {EpRequirements} from "../../../sdk/ep-requirements";
+import {DashboardWidgetSettings} from "../../../../core-model/dashboard/DashboardWidgetSettings";
+import {WidgetConfig} from "../base/base-config";
+
+export class MapConfig extends WidgetConfig {
+
+    static readonly LATITUDE_MAPPING_KEY: string = "latitude-mapping";
+    static readonly LONGITUDE_MAPPING_KEY: string = "longitude-mapping";
+    static readonly ITEMS_MAPPING_KEY: string = "items-mapping";
+    static readonly MARKER_TYPE_KEY: string = "marker-type-mapping";
+
+    constructor() {
+        super();
+    }
+
+    getConfig(): DashboardWidgetSettings {
+        return WidgetConfigBuilder.createWithSelectableColorsAndTitlePanel("map", "map")
+            .requiredSchema(SchemaRequirementsBuilder
+                .create()
+                .requiredPropertyWithUnaryMapping(MapConfig.LATITUDE_MAPPING_KEY, "Latitude field", "", EpRequirements.latitudeReq())
+                .requiredPropertyWithUnaryMapping(MapConfig.LONGITUDE_MAPPING_KEY, "Latitude field", "", EpRequirements.longitudeReq())
+                .requiredPropertyWithNaryMapping(MapConfig.ITEMS_MAPPING_KEY, "Fields to display", "", EpRequirements.anyProperty())
+                .build())
+            .requiredSingleValueSelection(MapConfig.MARKER_TYPE_KEY, "Marker type", "", ["Default", "Car"])
+            .build();
+    }
+
+}
\ No newline at end of file
diff --git a/ui/src/app/dashboard-v2/components/widgets/map/map-widget.component.css b/ui/src/app/dashboard-v2/components/widgets/map/map-widget.component.css
new file mode 100644
index 0000000..53ea75a
--- /dev/null
+++ b/ui/src/app/dashboard-v2/components/widgets/map/map-widget.component.css
@@ -0,0 +1,31 @@
+/*
+ *   Licensed to the Apache Software Foundation (ASF) under one or more
+ *   contributor license agreements.  See the NOTICE file distributed with
+ *   this work for additional information regarding copyright ownership.
+ *   The ASF licenses this file to You under the Apache License, Version 2.0
+ *   (the "License"); you may not use this file except in compliance with
+ *   the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ */
+
+.main-panel {
+    width:100%;
+    height: 100%;
+    display:inline-grid;
+    align-content: center;
+}
+
+.mt-20 {
+    margin: 10px;
+}
+
+.title-panel {
+    font-size:20px;
+}
\ No newline at end of file
diff --git a/ui/src/app/dashboard-v2/components/widgets/map/map-widget.component.html b/ui/src/app/dashboard-v2/components/widgets/map/map-widget.component.html
new file mode 100644
index 0000000..d159168
--- /dev/null
+++ b/ui/src/app/dashboard-v2/components/widgets/map/map-widget.component.html
@@ -0,0 +1,28 @@
+<!--
+  ~   Licensed to the Apache Software Foundation (ASF) under one or more
+  ~   contributor license agreements.  See the NOTICE file distributed with
+  ~   this work for additional information regarding copyright ownership.
+  ~   The ASF licenses this file to You under the Apache License, Version 2.0
+  ~   (the "License"); you may not use this file except in compliance with
+  ~   the License.  You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~   Unless required by applicable law or agreed to in writing, software
+  ~   distributed under the License is distributed on an "AS IS" BASIS,
+  ~   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~   See the License for the specific language governing permissions and
+  ~   limitations under the License.
+  -->
+
+<div fxFlex="100" fxLayoutAlign="center center" fxLayout="column" class="main-panel" [ngStyle]="{'background-color': selectedBackgroundColor, 'color': selectedPrimaryTextColor}">
+    <div class="title-panel mt-20" *ngIf="hasTitlePanelSettings">
+        {{selectedTitle}}
+    </div>
+    <div [ngStyle]="{'width': mapWidth + 'px', 'height': mapHeight + 'px'}"
+         leaflet
+         [leafletOptions]="options"
+         (leafletMapReady)="onMapReady($event)">
+        <div *ngIf="showMarkers" [leafletLayer]="markerLayer"></div>
+    </div>
+</div>
diff --git a/ui/src/app/dashboard-v2/components/widgets/map/map-widget.component.ts b/ui/src/app/dashboard-v2/components/widgets/map/map-widget.component.ts
new file mode 100644
index 0000000..61da91c
--- /dev/null
+++ b/ui/src/app/dashboard-v2/components/widgets/map/map-widget.component.ts
@@ -0,0 +1,122 @@
+/*
+ *   Licensed to the Apache Software Foundation (ASF) under one or more
+ *   contributor license agreements.  See the NOTICE file distributed with
+ *   this work for additional information regarding copyright ownership.
+ *   The ASF licenses this file to You under the Apache License, Version 2.0
+ *   (the "License"); you may not use this file except in compliance with
+ *   the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *   Unless required by applicable law or agreed to in writing, software
+ *   distributed under the License is distributed on an "AS IS" BASIS,
+ *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *   See the License for the specific language governing permissions and
+ *   limitations under the License.
+ */
+
+import {Component, OnDestroy, OnInit} from "@angular/core";
+import {RxStompService} from "@stomp/ng2-stompjs";
+import {BaseStreamPipesWidget} from "../base/base-widget";
+import {StaticPropertyExtractor} from "../../../sdk/extractor/static-property-extractor";
+import {MapConfig} from "./map-config";
+import {latLng, marker, Marker, tileLayer, Map, LatLngExpression, LatLng, icon, Content} from "leaflet";
+import {ResizeService} from "../../../services/resize.service";
+
+@Component({
+    selector: 'map-widget',
+    templateUrl: './map-widget.component.html',
+    styleUrls: ['./map-widget.component.css']
+})
+export class MapWidgetComponent extends BaseStreamPipesWidget implements OnInit, OnDestroy {
+
+    item: any;
+
+    selectedLatitudeField: string;
+    selectedLongitudeField: string;
+    selectedMarkerIcon: string;
+    additionalItemsToDisplay: Array<string>;
+
+    map: Map;
+    showMarkers: boolean = false;
+    markerLayer: Marker;
+
+    mapWidth: number;
+    mapHeight: number;
+
+    options = {
+        layers: [
+            tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 18, attribution: "© <a href=\'https://www.openstreetmap.org/copyright\'>OpenStreetMap</a> Contributors" })
+        ],
+        zoom: 5,
+        center: latLng(46.879966, -121.726909)
+    };
+
+    constructor(rxStompService: RxStompService, resizeService: ResizeService) {
+        super(rxStompService, resizeService, false);
+    }
+
+    ngOnInit(): void {
+        super.ngOnInit();
+        this.markerLayer = this.makeMarker([0, 0]);
+        this.showMarkers = true;
+        this.mapWidth = this.computeCurrentWidth(this.gridsterItemComponent);
+        this.mapHeight = this.computeCurrentHeight(this.gridsterItemComponent);
+    }
+
+    ngOnDestroy(): void {
+        super.ngOnDestroy();
+    }
+
+    extractConfig(extractor: StaticPropertyExtractor) {
+        this.selectedLatitudeField = extractor.mappingPropertyValue(MapConfig.LATITUDE_MAPPING_KEY);
+        this.selectedLongitudeField = extractor.mappingPropertyValue(MapConfig.LONGITUDE_MAPPING_KEY);
+        this.selectedMarkerIcon = this.markerImage(extractor.selectedSingleValue(MapConfig.MARKER_TYPE_KEY));
+        this.additionalItemsToDisplay = extractor.mappingPropertyValues(MapConfig.ITEMS_MAPPING_KEY);
+    }
+
+    markerImage(selectedMarker: string): string {
+        return selectedMarker === "Default" ? 'assets/img/marker-icon.png' : 'assets/img/pe_icons/car.png';
+    }
+
+    onMapReady(map: Map) {
+        this.map = map;
+        this.map.invalidateSize();
+    }
+
+    protected onEvent(event: any) {
+        this.updatePosition(event);
+    }
+
+    updatePosition(event) {
+        var lat = event[this.selectedLatitudeField];
+        var long = event[this.selectedLongitudeField];
+        var text = "";
+        this.additionalItemsToDisplay.forEach(item => {
+            text =  text.concat("<b>" +item +"</b>" +  ": " + event[item] + "<br>");
+        });
+
+        let content : Content = text;
+        let point: LatLngExpression = new LatLng(lat, long);
+        this.markerLayer.setLatLng(point);
+        this.markerLayer.bindTooltip(content);
+        this.map.panTo(point);
+
+    };
+
+    makeMarker(point: LatLngExpression): Marker {
+        return marker(point, { icon: icon({
+                iconSize: [ 25, 41 ],
+                iconAnchor: [ 13, 41 ],
+                iconUrl: this.selectedMarkerIcon,
+                shadowUrl: 'assets/img/marker-shadow.png'
+            })});
+    }
+
+    protected onSizeChanged(width: number, height: number) {
+        this.mapWidth = width;
+        this.mapHeight = height;
+        this.map.invalidateSize();
+    }
+
+}
\ No newline at end of file
diff --git a/ui/src/app/dashboard-v2/components/widgets/number/number-widget.component.ts b/ui/src/app/dashboard-v2/components/widgets/number/number-widget.component.ts
index 75421c9..28e1373 100644
--- a/ui/src/app/dashboard-v2/components/widgets/number/number-widget.component.ts
+++ b/ui/src/app/dashboard-v2/components/widgets/number/number-widget.component.ts
@@ -21,6 +21,7 @@ import {RxStompService} from "@stomp/ng2-stompjs";
 import {BaseStreamPipesWidget} from "../base/base-widget";
 import {StaticPropertyExtractor} from "../../../sdk/extractor/static-property-extractor";
 import {NumberConfig} from "./number-config";
+import {ResizeService} from "../../../services/resize.service";
 
 @Component({
     selector: 'number-widget',
@@ -33,8 +34,8 @@ export class NumberWidgetComponent extends BaseStreamPipesWidget implements OnIn
 
     selectedProperty: string;
 
-    constructor(rxStompService: RxStompService) {
-        super(rxStompService);
+    constructor(rxStompService: RxStompService, resizeService: ResizeService) {
+        super(rxStompService, resizeService, false);
     }
 
     ngOnInit(): void {
@@ -57,4 +58,7 @@ export class NumberWidgetComponent extends BaseStreamPipesWidget implements OnIn
         this.item = event[this.selectedProperty];
     }
 
+    protected onSizeChanged(width: number, height: number) {
+    }
+
 }
\ No newline at end of file
diff --git a/ui/src/app/dashboard-v2/components/widgets/table/table-widget.component.ts b/ui/src/app/dashboard-v2/components/widgets/table/table-widget.component.ts
index e6e37aa..17185f2 100644
--- a/ui/src/app/dashboard-v2/components/widgets/table/table-widget.component.ts
+++ b/ui/src/app/dashboard-v2/components/widgets/table/table-widget.component.ts
@@ -23,6 +23,7 @@ import {StaticPropertyExtractor} from "../../../sdk/extractor/static-property-ex
 import {MatTableDataSource} from "@angular/material/table";
 import {TableConfig} from "./table-config";
 import {SemanticTypeUtilsService} from "../../../../core-services/semantic-type/semantic-type-utils.service";
+import {ResizeService} from "../../../services/resize.service";
 
 @Component({
     selector: 'table-widget',
@@ -37,8 +38,8 @@ export class TableWidgetComponent extends BaseStreamPipesWidget implements OnIni
     dataSource = new MatTableDataSource();
     semanticTypes: { [key: string]: string; } = {};
 
-    constructor(rxStompService: RxStompService, private semanticTypeUtils: SemanticTypeUtilsService) {
-        super(rxStompService);
+    constructor(rxStompService: RxStompService, resizeService: ResizeService, private semanticTypeUtils: SemanticTypeUtilsService) {
+        super(rxStompService, resizeService, false);
     }
 
     ngOnInit(): void {
@@ -74,4 +75,7 @@ export class TableWidgetComponent extends BaseStreamPipesWidget implements OnIni
         return object;
     }
 
+    protected onSizeChanged(width: number, height: number) {
+    }
+
 }
\ No newline at end of file
diff --git a/ui/src/app/dashboard-v2/dashboard.module.ts b/ui/src/app/dashboard-v2/dashboard.module.ts
index 5aed5d1..5d806e9 100644
--- a/ui/src/app/dashboard-v2/dashboard.module.ts
+++ b/ui/src/app/dashboard-v2/dashboard.module.ts
@@ -49,6 +49,8 @@ import {SemanticTypeUtilsService} from '../core-services/semantic-type/semantic-
 import {GaugeWidgetComponent} from "./components/widgets/gauge/gauge-widget.component";
 import {ImageWidgetComponent} from "./components/widgets/image/image-widget.component";
 import {AreaWidgetComponent} from "./components/widgets/area/area-widget.component";
+import {MapWidgetComponent} from "./components/widgets/map/map-widget.component";
+import {LeafletModule} from "@asymmetrik/ngx-leaflet";
 
 const dashboardWidgets = [
 
@@ -72,6 +74,7 @@ const dashboardWidgets = [
         ConnectModule,
         NgxChartsModule,
         CdkTableModule,
+        LeafletModule
     ],
     declarations: [
         DashboardComponent,
@@ -86,7 +89,8 @@ const dashboardWidgets = [
         NumberWidgetComponent,
         TableWidgetComponent,
         GaugeWidgetComponent,
-        ImageWidgetComponent
+        ImageWidgetComponent,
+        MapWidgetComponent
     ],
     providers: [
         DashboardService,
diff --git a/ui/src/app/dashboard-v2/dialogs/add-widget/add-visualization-dialog.component.ts b/ui/src/app/dashboard-v2/dialogs/add-widget/add-visualization-dialog.component.ts
index 58ec6ad..8ea4895 100644
--- a/ui/src/app/dashboard-v2/dialogs/add-widget/add-visualization-dialog.component.ts
+++ b/ui/src/app/dashboard-v2/dialogs/add-widget/add-visualization-dialog.component.ts
@@ -81,7 +81,7 @@ export class AddVisualizationDialogComponent {
             this.availableWidgets = WidgetRegistry.getAvailableWidgetTemplates();
         } else {
             this.dialogTitle = "Edit widget";
-            this.selectedPipeline = this.data.widget.dashboardWidgetDataConfig;
+            this.selectedPipeline = this.data.pipeline;
             this.selectedWidget = this.data.widget.dashboardWidgetSettings;
             this.page = 'configure-widget';
         }
diff --git a/ui/src/app/dashboard-v2/registry/widget-config-builder.ts b/ui/src/app/dashboard-v2/registry/widget-config-builder.ts
index ed9a5f8..e7911a4 100644
--- a/ui/src/app/dashboard-v2/registry/widget-config-builder.ts
+++ b/ui/src/app/dashboard-v2/registry/widget-config-builder.ts
@@ -21,6 +21,9 @@ import {CollectedSchemaRequirements} from "../sdk/collected-schema-requirements"
 import {DashboardWidgetSettings} from "../../core-model/dashboard/DashboardWidgetSettings";
 import {Datatypes} from "../sdk/model/datatypes";
 import {ColorPickerStaticProperty} from "../../connect/model/ColorPickerStaticProperty";
+import {OneOfStaticProperty} from "../../connect/model/OneOfStaticProperty";
+import {StaticProperty} from "../../connect/model/StaticProperty";
+import {Option} from "../../connect/model/Option";
 
 export class WidgetConfigBuilder {
 
@@ -64,7 +67,7 @@ export class WidgetConfigBuilder {
     }
 
     requiredTextParameter(id: string, label: string, description: string): WidgetConfigBuilder {
-        let fst: FreeTextStaticProperty = this.prepareStaticProperty(id, label, description, Datatypes.String.toUri())
+        let fst: FreeTextStaticProperty = this.prepareFreeTextStaticProperty(id, label, description, Datatypes.String.toUri())
         this.widget.config.push(fst);
         return this;
     }
@@ -83,13 +86,27 @@ export class WidgetConfigBuilder {
 
 
     requiredIntegerParameter(id: string, label: string, description: string): WidgetConfigBuilder {
-        let fst: FreeTextStaticProperty = this.prepareStaticProperty(id, label, description, Datatypes.Integer.toUri())
+        let fst: FreeTextStaticProperty = this.prepareFreeTextStaticProperty(id, label, description, Datatypes.Integer.toUri())
         this.widget.config.push(fst);
         return this;
     }
 
+    requiredSingleValueSelection(id: string, label: string, description: string, options: Array<string>): WidgetConfigBuilder {
+        let osp: OneOfStaticProperty = new OneOfStaticProperty();
+        this.prepareStaticProperty(id, label, description, osp);
+
+        osp.options = [];
+        options.forEach(o => {
+            let option = new Option();
+            option.name = o;
+            osp.options.push(option);
+        });
+        this.widget.config.push(osp);
+        return this;
+    }
+
     requiredFloatParameter(id: string, label: string, description: string): WidgetConfigBuilder {
-        let fst: FreeTextStaticProperty = this.prepareStaticProperty(id, label, description, Datatypes.Float.toUri())
+        let fst: FreeTextStaticProperty = this.prepareFreeTextStaticProperty(id, label, description, Datatypes.Float.toUri())
         this.widget.config.push(fst);
         return this;
     }
@@ -101,11 +118,15 @@ export class WidgetConfigBuilder {
         return this;
     }
 
-    prepareStaticProperty(id: string, label: string, description: string, datatype: string) {
+    prepareStaticProperty(id: string, label: string, description: string, sp: StaticProperty) {
+        sp.internalName = id;
+        sp.label = label;
+        sp.description = description;
+    }
+
+    prepareFreeTextStaticProperty(id: string, label: string, description: string, datatype: string) {
         let fst: FreeTextStaticProperty = new FreeTextStaticProperty();
-        fst.internalName = id;
-        fst.label = label;
-        fst.description = description;
+        this.prepareStaticProperty(id, label, description, fst);
         fst.requiredDatatype = datatype;
 
         return fst;
diff --git a/ui/src/app/dashboard-v2/registry/widget-registry.ts b/ui/src/app/dashboard-v2/registry/widget-registry.ts
index 3c62fe6..c00daf2 100644
--- a/ui/src/app/dashboard-v2/registry/widget-registry.ts
+++ b/ui/src/app/dashboard-v2/registry/widget-registry.ts
@@ -24,6 +24,7 @@ import {TableConfig} from "../components/widgets/table/table-config";
 import {GaugeConfig} from "../components/widgets/gauge/gauge-config";
 import {ImageConfig} from "../components/widgets/image/image-config";
 import {AreaConfig} from "../components/widgets/area/area-config";
+import {MapConfig} from "../components/widgets/map/map-config";
 
 export class WidgetRegistry {
 
@@ -33,7 +34,8 @@ export class WidgetRegistry {
         new TableConfig(),
         new GaugeConfig(),
         new ImageConfig(),
-        new AreaConfig()
+        new AreaConfig(),
+        new MapConfig()
     ];
 
     static getAvailableWidgetTemplates(): Array<DashboardWidgetSettings> {
diff --git a/ui/src/app/dashboard-v2/sdk/ep-requirements.ts b/ui/src/app/dashboard-v2/sdk/ep-requirements.ts
index b298a7a..5a2174f 100644
--- a/ui/src/app/dashboard-v2/sdk/ep-requirements.ts
+++ b/ui/src/app/dashboard-v2/sdk/ep-requirements.ts
@@ -19,6 +19,7 @@
 import {EventProperty} from "../../connect/schema-editor/model/EventProperty";
 import {EventPropertyPrimitive} from "../../connect/schema-editor/model/EventPropertyPrimitive";
 import {Datatypes} from "./model/datatypes";
+import {Vocabulary} from "./model/vocabulary";
 
 export class EpRequirements {
 
@@ -35,6 +36,14 @@ export class EpRequirements {
         return EpRequirements.domainPropertyReq("https://image.com");
     }
 
+    static latitudeReq(): EventProperty {
+        return EpRequirements.domainPropertyReq(Vocabulary.GEO + "lat");
+    }
+
+    static longitudeReq(): EventProperty {
+        return EpRequirements.domainPropertyReq(Vocabulary.GEO + "long")
+    }
+
     static timestampReq(): EventProperty {
         return EpRequirements.domainPropertyReq("http://schema.org/DateTime");
     }
diff --git a/ui/src/app/dashboard-v2/sdk/extractor/static-property-extractor.ts b/ui/src/app/dashboard-v2/sdk/extractor/static-property-extractor.ts
index 93d0adb..5d3c143 100644
--- a/ui/src/app/dashboard-v2/sdk/extractor/static-property-extractor.ts
+++ b/ui/src/app/dashboard-v2/sdk/extractor/static-property-extractor.ts
@@ -22,6 +22,7 @@ import {MappingPropertyUnary} from "../../../connect/model/MappingPropertyUnary"
 import {FreeTextStaticProperty} from "../../../connect/model/FreeTextStaticProperty";
 import {ColorPickerStaticProperty} from "../../../connect/model/ColorPickerStaticProperty";
 import {MappingPropertyNary} from "../../../connect/model/MappingPropertyNary";
+import {OneOfStaticProperty} from "../../../connect/model/OneOfStaticProperty";
 
 export class StaticPropertyExtractor {
 
@@ -42,6 +43,11 @@ export class StaticPropertyExtractor {
     mappingPropertyValues(internalId: string): Array<string> {
         let sp: MappingPropertyNary = this.getStaticPropertyByName(internalId) as MappingPropertyNary;
         let properties: Array<string> = [];
+        // TODO this quick-fixes a deserialization bug in Tson-LD
+        if (!Array.isArray(sp.selectedProperties)) {
+            let value: string = sp.selectedProperties as any;
+            sp.selectedProperties = [value];
+        }
         sp.selectedProperties.forEach(ep => {
            properties.push(this.removePrefix(ep));
         });
@@ -58,6 +64,11 @@ export class StaticPropertyExtractor {
         return sp.selectedColor;
     }
 
+    selectedSingleValue(internalId: string): string {
+        let sp: OneOfStaticProperty = this.getStaticPropertyByName(internalId) as OneOfStaticProperty;
+        return sp.options.find(o => o.selected).name;
+    }
+
     stringParameter(internalId: string): string {
         return this.singleValueParameter(internalId) as string;
     }
diff --git a/ui/src/app/dashboard-v2/sdk/model/vocabulary.ts b/ui/src/app/dashboard-v2/sdk/model/vocabulary.ts
index 175377b..90c0c9c 100644
--- a/ui/src/app/dashboard-v2/sdk/model/vocabulary.ts
+++ b/ui/src/app/dashboard-v2/sdk/model/vocabulary.ts
@@ -20,4 +20,5 @@ export class Vocabulary {
 
     static readonly XSD: string = "http://www.w3.org/2001/XMLSchema#";
     static readonly SO: string = "http://schema.org/";
+    static readonly GEO: string = "http://www.w3.org/2003/01/geo/wgs84_pos#"
 }
\ No newline at end of file
diff --git a/ui/src/scss/main.scss b/ui/src/scss/main.scss
index 9200d4f..34a9567 100644
--- a/ui/src/scss/main.scss
+++ b/ui/src/scss/main.scss
@@ -35,6 +35,7 @@
 @import '~prismjs/themes/prism.css';
 @import '~angular-loading-bar/build/loading-bar.min.css';
 @import '~shepherd.js/dist/css/shepherd-theme-default.css';
+@import '~leaflet/dist/leaflet.css';
 
 @import '../assets/fonts/MaterialIcons-Regular.css';
 @import '../assets/fonts/Roboto-Regular.css';