Geomap: Sync v9.1.x with main (#55428)

* sync geomap

* Geomap: Add devenv dashboard for v9.1 of geomap (#53638)

* Geomap: Spatial operations location options transform e2e test (#54162)

* Devenv: Geomap global vs local field coloring (#54253)

* Geomap: cleanup (#54328)

* Geomap: Layer types e2e (#54666)

* Geomap: Map control options e2e (#54809)

* update betterer

* add missing icon for measure tools

* Geomap: Update language to match documentation and remove beta (#53703)

Co-authored-by: nmarrs <nathanielmarrs@gmail.com>

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
Co-authored-by: Adela Almasan <88068998+adela-almasan@users.noreply.github.com>
Co-authored-by: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com>
This commit is contained in:
Nathan Marrs
2022-09-19 18:05:53 -07:00
committed by GitHub
co-authored by Ryan McKinley Adela Almasan Drew Slobodnjak
parent 89410ffd0c
commit 90364a9173
45 changed files with 2970 additions and 644 deletions
+21 -79
View File
@@ -2792,11 +2792,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/core/components/Layers/LayerDragDropList.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/core/components/Layers/LayerName.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
@@ -8674,40 +8670,17 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"public/app/plugins/panel/geomap/GeomapPanel.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Do not use any type assertions.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Do not use any type assertions.", "9"],
[0, 0, 0, "Do not use any type assertions.", "10"],
[0, 0, 0, "Do not use any type assertions.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Do not use any type assertions.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
[0, 0, 0, "Unexpected any. Specify a different type.", "17"]
],
"public/app/plugins/panel/geomap/components/DataHoverRows.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/panel/geomap/components/DataHoverView.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/panel/geomap/components/MarkersLegend.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Do not use any type assertions.", "6"]
[0, 0, 0, "Do not use any type assertions.", "4"]
],
"public/app/plugins/panel/geomap/components/MeasureVectorLayer.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"]
],
"public/app/plugins/panel/geomap/editor/FrameSelectionEditor.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
@@ -8715,59 +8688,26 @@ exports[`better eslint`] = {
],
"public/app/plugins/panel/geomap/editor/GeomapStyleRulesEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
],
"public/app/plugins/panel/geomap/editor/LayersEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
],
"public/app/plugins/panel/geomap/editor/MapViewEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"public/app/plugins/panel/geomap/editor/StyleEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Do not use any type assertions.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
[0, 0, 0, "Do not use any type assertions.", "6"],
[0, 0, 0, "Do not use any type assertions.", "7"],
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
[0, 0, 0, "Do not use any type assertions.", "8"],
[0, 0, 0, "Do not use any type assertions.", "9"],
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
[0, 0, 0, "Do not use any type assertions.", "10"],
[0, 0, 0, "Do not use any type assertions.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Do not use any type assertions.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Do not use any type assertions.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
[0, 0, 0, "Do not use any type assertions.", "17"],
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
[0, 0, 0, "Do not use any type assertions.", "19"],
[0, 0, 0, "Unexpected any. Specify a different type.", "20"],
[0, 0, 0, "Do not use any type assertions.", "21"],
[0, 0, 0, "Unexpected any. Specify a different type.", "22"],
[0, 0, 0, "Do not use any type assertions.", "23"],
[0, 0, 0, "Unexpected any. Specify a different type.", "24"],
[0, 0, 0, "Do not use any type assertions.", "25"],
[0, 0, 0, "Unexpected any. Specify a different type.", "26"]
[0, 0, 0, "Do not use any type assertions.", "12"]
],
"public/app/plugins/panel/geomap/editor/StyleRuleEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Do not use any type assertions.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
],
"public/app/plugins/panel/geomap/editor/layerEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"public/app/plugins/panel/geomap/layers/basemaps/carto.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
@@ -8790,18 +8730,20 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
],
"public/app/plugins/panel/geomap/module.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/panel/geomap/style/utils.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/panel/geomap/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
"public/app/plugins/panel/geomap/utils/layers.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"public/app/plugins/panel/geomap/utils/selection.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/plugins/panel/geomap/utils/tootltip.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"public/app/plugins/panel/geomap/view.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
@@ -0,0 +1,363 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 17,
"w": 9,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"basemap": {
"config": {
"server": "ocean"
},
"name": "Layer 0",
"type": "esri-xyz"
},
"controls": {
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showMeasure": false,
"showScale": false,
"showZoom": true
},
"layers": [
{
"config": {
"server": "usa-topo"
},
"name": "Topographic map",
"opacity": 0.8,
"tooltip": true,
"type": "esri-xyz"
}
],
"tooltip": {
"mode": "details"
},
"view": {
"id": "coords",
"lat": 26.076593,
"lon": 113.075128,
"zoom": 3.76
}
},
"pluginVersion": "9.2.0-pre",
"targets": [
{
"csvFileName": "flight_info_by_state.csv",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "csv_file"
},
{
"csvFileName": "population_by_state.csv",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "B",
"scenarioId": "csv_file"
}
],
"title": "Multiple map layers",
"type": "geomap"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 17,
"w": 15,
"x": 9,
"y": 0
},
"id": 4,
"options": {
"basemap": {
"config": {
"server": "world-imagery"
},
"name": "Layer 0",
"type": "esri-xyz"
},
"controls": {
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showMeasure": false,
"showScale": false,
"showZoom": true
},
"layers": [
{
"config": {
"nightColor": "#000000",
"show": "to",
"sun": true
},
"location": {
"mode": "auto"
},
"name": "Day / night",
"opacity": 0.5,
"tooltip": true,
"type": "dayNight"
}
],
"tooltip": {
"mode": "details"
},
"view": {
"id": "coords",
"lat": 29.633972,
"lon": -17.661858,
"zoom": 2.39
}
},
"pluginVersion": "9.2.0-pre",
"title": "Day / night layer",
"type": "geomap"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 16,
"w": 14,
"x": 5,
"y": 17
},
"id": 6,
"options": {
"basemap": {
"config": {},
"name": "Layer 0",
"type": "default"
},
"controls": {
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showMeasure": true,
"showScale": false,
"showZoom": true
},
"layers": [
{
"config": {
"style": {
"color": {
"fixed": "dark-green"
},
"lineWidth": 2,
"opacity": 1,
"rotation": {
"fixed": 0,
"max": 360,
"min": -360,
"mode": "mod"
},
"size": {
"fixed": 5,
"max": 15,
"min": 2
},
"symbol": {
"fixed": "img/icons/marker/circle.svg",
"mode": "fixed"
},
"textConfig": {
"fontSize": 12,
"offsetX": 0,
"offsetY": 0,
"textAlign": "center",
"textBaseline": "middle"
}
}
},
"name": "Flight path",
"tooltip": true,
"type": "route"
}
],
"tooltip": {
"mode": "details"
},
"view": {
"id": "coords",
"lat": 37.829114,
"lon": -122.439462,
"zoom": 11.92
}
},
"pluginVersion": "9.2.0-pre",
"targets": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "simulation",
"sim": {
"key": {
"tick": 10,
"type": "flight"
}
}
}
],
"title": "Route layer",
"type": "geomap"
}
],
"schemaVersion": 37,
"style": "dark",
"tags": [
"gdev",
"panel-tests",
"geomap"
],
"templating": {
"list": []
},
"time": {
"from": "now-5m",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Panel Tests - Geomap 9.1",
"uid": "KVxZKdmVz",
"version": 10,
"weekStart": ""
}
@@ -0,0 +1,503 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-BlYlRd"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 15,
"w": 10,
"x": 0,
"y": 0
},
"id": 2,
"options": {
"basemap": {
"config": {},
"name": "Layer 0",
"type": "default"
},
"controls": {
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showMeasure": false,
"showScale": false,
"showZoom": true
},
"layers": [
{
"config": {
"showLegend": true,
"style": {
"color": {
"field": "Count",
"fixed": "dark-green"
},
"opacity": 0.4,
"rotation": {
"fixed": 0,
"max": 360,
"min": -360,
"mode": "mod"
},
"size": {
"fixed": 5,
"max": 15,
"min": 2
},
"symbol": {
"fixed": "img/icons/marker/circle.svg",
"mode": "fixed"
},
"textConfig": {
"fontSize": 12,
"offsetX": 0,
"offsetY": 0,
"textAlign": "center",
"textBaseline": "middle"
}
}
},
"location": {
"mode": "auto"
},
"name": "Layer 1",
"tooltip": true,
"type": "markers"
}
],
"tooltip": {
"mode": "details"
},
"view": {
"id": "zero",
"lat": 0,
"lon": 0,
"zoom": 1
}
},
"pluginVersion": "9.2.0-pre",
"targets": [
{
"csvFileName": "flight_info_by_state.csv",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "csv_file"
}
],
"title": "Geomap - auto min/max",
"type": "geomap"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-BlYlRd"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"mappings": [],
"max": 15,
"min": 1,
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 15,
"w": 10,
"x": 10,
"y": 0
},
"id": 7,
"options": {
"basemap": {
"config": {},
"name": "Layer 0",
"type": "default"
},
"controls": {
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showMeasure": false,
"showScale": false,
"showZoom": true
},
"layers": [
{
"config": {
"showLegend": true,
"style": {
"color": {
"field": "Count",
"fixed": "dark-green"
},
"opacity": 0.4,
"rotation": {
"fixed": 0,
"max": 360,
"min": -360,
"mode": "mod"
},
"size": {
"fixed": 5,
"max": 15,
"min": 2
},
"symbol": {
"fixed": "img/icons/marker/circle.svg",
"mode": "fixed"
},
"textConfig": {
"fontSize": 12,
"offsetX": 0,
"offsetY": 0,
"textAlign": "center",
"textBaseline": "middle"
}
}
},
"location": {
"mode": "auto"
},
"name": "Layer 1",
"tooltip": true,
"type": "markers"
}
],
"tooltip": {
"mode": "details"
},
"view": {
"id": "zero",
"lat": 0,
"lon": 0,
"zoom": 1
}
},
"pluginVersion": "9.2.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "A"
}
],
"title": "Geomap - min/max",
"type": "geomap"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-BlYlRd"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 15,
"w": 2,
"x": 20,
"y": 0
},
"id": 6,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"min"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "9.2.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "A"
}
],
"title": "Min values",
"type": "stat"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-BlYlRd"
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 15,
"w": 2,
"x": 22,
"y": 0
},
"id": 5,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"max"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "9.2.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "A"
}
],
"title": "Max values",
"type": "stat"
},
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-BlYlRd"
},
"custom": {
"align": "auto",
"displayMode": "auto",
"inspect": false
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": [
{
"matcher": {
"id": "byName",
"options": "Count"
},
"properties": [
{
"id": "custom.displayMode",
"value": "color-background"
}
]
},
{
"matcher": {
"id": "byName",
"options": "Price"
},
"properties": [
{
"id": "custom.displayMode",
"value": "color-background"
}
]
}
]
},
"gridPos": {
"h": 11,
"w": 24,
"x": 0,
"y": 15
},
"id": 4,
"options": {
"footer": {
"fields": "",
"reducer": [
"max"
],
"show": true
},
"showHeader": true
},
"pluginVersion": "9.2.0-pre",
"targets": [
{
"datasource": {
"type": "datasource",
"uid": "-- Dashboard --"
},
"panelId": 2,
"refId": "A"
}
],
"title": "Panel Title",
"type": "table"
}
],
"schemaVersion": 37,
"style": "dark",
"tags": [
"gdev",
"panel-tests",
"geomap"
],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Geomap - color field Copy",
"uid": "XMuLlpZ4k",
"version": 1,
"weekStart": ""
}
@@ -0,0 +1,125 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
}
},
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"id": 1,
"options": {
"controls": {
"mouseWheelZoom": true,
"showAttribution": false,
"showDebug": false,
"showMeasure": false,
"showScale": false,
"showZoom": false
},
"tooltip": {
"mode": "details"
},
"view": {
"id": "zero",
"lat": 0,
"lon": 0,
"zoom": 1
}
},
"pluginVersion": "9.2.0-pre",
"targets": [
{
"csvFileName": "flight_info_by_state.csv",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "csv_file"
}
],
"title": "Geomap/Geohash",
"type": "geomap"
}
],
"schemaVersion": 37,
"style": "dark",
"tags": [
"gdev",
"panel-tests",
"geomap"
],
"templating": {
"list": []
},
"time": {
"from": "now-5m",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Panel Tests - Geomap geohash transformer",
"uid": "P2jR04WVk",
"version": 10,
"weekStart": ""
}
@@ -440,8 +440,8 @@
"style": "dark",
"tags": [
"gdev",
"geomap",
"panel-tests"
"panel-tests",
"geomap"
],
"templating": {
"list": []
@@ -24,7 +24,6 @@
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 83,
"links": [],
"liveNow": false,
"panels": [
@@ -50,7 +49,8 @@
"mode": "absolute",
"steps": [
{
"color": "green"
"color": "green",
"value": null
},
{
"color": "red",
@@ -78,6 +78,7 @@
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showMeasure": false,
"showScale": false,
"showZoom": true
},
@@ -107,6 +108,9 @@
"type": "markers"
}
],
"tooltip": {
"mode": "details"
},
"view": {
"id": "coords",
"lat": 38.297683,
@@ -115,7 +119,7 @@
"zoom": 3.98
}
},
"pluginVersion": "9.0.0-pre",
"pluginVersion": "9.2.0-pre",
"targets": [
{
"csvFileName": "flight_info_by_state.csv",
@@ -148,7 +152,8 @@
"mode": "absolute",
"steps": [
{
"color": "green"
"color": "green",
"value": null
},
{
"color": "red",
@@ -180,6 +185,7 @@
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showMeasure": false,
"showScale": false,
"showZoom": true
},
@@ -209,6 +215,9 @@
"type": "markers"
}
],
"tooltip": {
"mode": "details"
},
"view": {
"id": "coords",
"lat": 38.297683,
@@ -217,7 +226,7 @@
"zoom": 3.98
}
},
"pluginVersion": "9.0.0-pre",
"pluginVersion": "9.2.0-pre",
"targets": [
{
"csvFileName": "flight_info_by_state.csv",
@@ -250,7 +259,8 @@
"mode": "absolute",
"steps": [
{
"color": "green"
"color": "green",
"value": null
},
{
"color": "red",
@@ -278,6 +288,7 @@
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showMeasure": false,
"showScale": false,
"showZoom": true
},
@@ -302,6 +313,9 @@
"type": "heatmap"
}
],
"tooltip": {
"mode": "details"
},
"view": {
"id": "coords",
"lat": 38.251497,
@@ -310,7 +324,7 @@
"zoom": 4.15
}
},
"pluginVersion": "9.0.0-pre",
"pluginVersion": "9.2.0-pre",
"targets": [
{
"csvFileName": "flight_info_by_state.csv",
@@ -344,7 +358,8 @@
"mode": "absolute",
"steps": [
{
"color": "green"
"color": "green",
"value": null
},
{
"color": "red",
@@ -374,6 +389,7 @@
"mouseWheelZoom": true,
"showAttribution": true,
"showDebug": false,
"showMeasure": false,
"showScale": false,
"showZoom": true
},
@@ -402,6 +418,9 @@
"type": "markers"
}
],
"tooltip": {
"mode": "details"
},
"view": {
"id": "coords",
"lat": 40.159084,
@@ -410,7 +429,7 @@
"zoom": 3.83
}
},
"pluginVersion": "9.0.0-pre",
"pluginVersion": "9.2.0-pre",
"targets": [
{
"csvFileName": "flight_info_by_state.csv",
@@ -423,11 +442,12 @@
}
],
"refresh": "",
"schemaVersion": 36,
"schemaVersion": 37,
"style": "dark",
"tags": [
"gdev",
"panel-tests"
"panel-tests",
"geomap"
],
"templating": {
"list": []
@@ -452,6 +472,6 @@
"timezone": "",
"title": "Panel Tests - Geomap",
"uid": "2xuwrgV7z",
"version": 2,
"version": 4,
"weekStart": ""
}
}
@@ -0,0 +1,322 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "datasource",
"uid": "grafana"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"target": {
"limit": 100,
"matchAny": false,
"tags": [],
"type": "dashboard"
},
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 1348,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 0
},
"id": 4,
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false
},
"content": "## Data center = $datacenter\n\n### server = $server\n\n#### pod = $pod\n\n---\ntext = $Text",
"mode": "markdown"
},
"pluginVersion": "9.2.0-pre",
"targets": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "random_walk"
}
],
"title": "Markdown (with variables)",
"type": "text"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 0
},
"id": 5,
"options": {
"code": {
"language": "json",
"showLineNumbers": true,
"showMiniMap": false
},
"content": "{\n \"datacenter\": $datacenter,\n \"server\": $server,\n \"pod\": $pod\n \"text\": $Text\n}\n",
"mode": "code"
},
"pluginVersion": "9.2.0-pre",
"targets": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "random_walk"
}
],
"title": "JSON (with variables)",
"type": "text"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 9
},
"id": 6,
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false
},
"content": "<h3>Data center</h3>\n<p>$datacenter</p>\n\n<h3>server</h3>\n<p>$server</p>\n\n<h3>pod</h3>\n<p>$pod</p>\n\n<h3>Text</h3>\n<p>$Text</p>",
"mode": "html"
},
"pluginVersion": "9.2.0-pre",
"targets": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "random_walk"
}
],
"title": "HTML (with variables)",
"type": "text"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"gridPos": {
"h": 9,
"w": 12,
"x": 12,
"y": 9
},
"id": 7,
"options": {
"code": {
"language": "markdown",
"showLineNumbers": true,
"showMiniMap": true
},
"content": "## Data center\n$datacenter\n\n### server\n$server\n\n#### pod = \n$pod\n",
"mode": "code"
},
"pluginVersion": "9.2.0-pre",
"targets": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "random_walk"
}
],
"title": "Markdown (code w/ with variables)",
"type": "text"
}
],
"refresh": false,
"schemaVersion": 37,
"style": "dark",
"tags": [],
"templating": {
"list": [
{
"current": {
"selected": false,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"definition": "*",
"hide": 0,
"includeAll": true,
"multi": true,
"name": "datacenter",
"options": [],
"query": {
"query": "*",
"refId": "gdev-testdata-datacenter-Variable-Query"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"tagValuesQuery": "",
"tagsQuery": "",
"type": "query",
"useTags": false
},
{
"current": {
"selected": false,
"text": [
"All"
],
"value": [
"$__all"
]
},
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"definition": "$datacenter.*",
"hide": 0,
"includeAll": true,
"multi": true,
"name": "server",
"options": [],
"query": {
"query": "$datacenter.*",
"refId": "gdev-testdata-server-Variable-Query"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"tagValuesQuery": "",
"tagsQuery": "",
"type": "query",
"useTags": false
},
{
"current": {
"selected": false,
"text": [
"AAA",
"ACB"
],
"value": [
"AAA",
"ACB"
]
},
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"definition": "$datacenter.$server.*",
"hide": 0,
"includeAll": true,
"multi": true,
"name": "pod",
"options": [],
"query": {
"query": "$datacenter.$server.*",
"refId": "gdev-testdata-pod-Variable-Query"
},
"refresh": 1,
"regex": "",
"skipUrlSync": false,
"sort": 0,
"tagValuesQuery": "",
"tagsQuery": "",
"type": "query",
"useTags": false
},
{
"current": {
"selected": false,
"text": "temp",
"value": "temp"
},
"hide": 0,
"name": "Text",
"options": [
{
"selected": true,
"text": "temp",
"value": "temp"
}
],
"query": "temp",
"skipUrlSync": false,
"type": "textbox"
}
]
},
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
]
},
"timezone": "",
"title": "Text options",
"uid": "WZ7AhQiVz",
"version": 1,
"weekStart": ""
}
+107
View File
@@ -0,0 +1,107 @@
import { e2e } from '@grafana/e2e';
const DASHBOARD_ID = 'P2jR04WVk';
const MAP_LAYERS_TYPE = 'Map layers Layer type';
const MAP_LAYERS_DATA = 'Map layers Data';
const MAP_LAYERS_GEOJSON = 'Map layers GeoJSON URL';
e2e.scenario({
describeName: 'Geomap layer types',
itName: 'Tests changing the layer type',
addScenarioDataSource: false,
addScenarioDashBoard: false,
skipScenario: false,
scenario: () => {
e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } });
cy.get('[data-testid="layer-drag-drop-list"]').should('be.visible');
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).should('be.visible');
cy.get('[data-testid="layer-drag-drop-list"]').contains('markers');
// Heatmap
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Heatmap{enter}');
cy.get('[data-testid="layer-drag-drop-list"]').contains('heatmap');
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible');
e2e.components.PanelEditor.General.content().should('be.visible');
// GeoJSON
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('GeoJSON{enter}');
cy.get('[data-testid="layer-drag-drop-list"]').contains('geojson');
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('not.exist');
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON).should('be.visible');
e2e.components.PanelEditor.General.content().should('be.visible');
// Open Street Map
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Open Street Map{enter}');
cy.get('[data-testid="layer-drag-drop-list"]').contains('osm-standard');
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('not.exist');
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON).should('not.exist');
e2e.components.PanelEditor.General.content().should('be.visible');
// CARTO basemap
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('CARTO basemap{enter}');
cy.get('[data-testid="layer-drag-drop-list"]').contains('carto');
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show labels').should('be.visible');
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Theme').should('be.visible');
e2e.components.PanelEditor.General.content().should('be.visible');
// ArcGIS MapServer
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('ArcGIS MapServer{enter}');
cy.get('[data-testid="layer-drag-drop-list"]').contains('esri-xyz');
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Server instance').should('be.visible');
e2e.components.PanelEditor.General.content().should('be.visible');
// XYZ Tile layer
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('XYZ Tile layer{enter}');
cy.get('[data-testid="layer-drag-drop-list"]').contains('xyz');
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers URL template').should('be.visible');
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Attribution').should('be.visible');
e2e.components.PanelEditor.General.content().should('be.visible');
},
});
e2e.scenario({
describeName: 'Geomap layer types (alpha)',
itName: 'Tests changing the layer type (alpha)',
addScenarioDataSource: false,
addScenarioDashBoard: false,
skipScenario: true,
scenario: () => {
e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } });
cy.get('[data-testid="layer-drag-drop-list"]').should('be.visible');
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).should('be.visible');
cy.get('[data-testid="layer-drag-drop-list"]').contains('markers');
// Icon at last point (Alpha)
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Icon at last point{enter}');
cy.get('[data-testid="layer-drag-drop-list"]').contains('last-point-tracker');
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible');
e2e.components.PanelEditor.General.content().should('be.visible');
// Dynamic GeoJSON (Alpha)
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Dynamic GeoJSON{enter}');
cy.get('[data-testid="layer-drag-drop-list"]').contains('dynamic-geojson');
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible');
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_GEOJSON).should('be.visible');
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers ID Field').should('be.visible');
e2e.components.PanelEditor.General.content().should('be.visible');
// Night / Day (Alpha)
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Night / Day{enter}');
cy.get('[data-testid="layer-drag-drop-list"]').contains('dayNight');
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible');
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Show').should('be.visible');
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Night region color').should('be.visible');
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Display sun').should('be.visible');
e2e.components.PanelEditor.General.content().should('be.visible');
// Route (Alpha)
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_TYPE).type('Route{enter}');
cy.get('[data-testid="layer-drag-drop-list"]').contains('route');
e2e.components.PanelEditor.OptionsPane.fieldLabel(MAP_LAYERS_DATA).should('be.visible');
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Location').should('be.visible');
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Style').should('be.visible');
e2e.components.PanelEditor.OptionsPane.fieldLabel('Map layers Line width').should('be.visible');
e2e.components.PanelEditor.General.content().should('be.visible');
},
});
@@ -0,0 +1,35 @@
import { e2e } from '@grafana/e2e';
const DASHBOARD_ID = 'P2jR04WVk';
e2e.scenario({
describeName: 'Geomap map controls options',
itName: 'Tests map controls options',
addScenarioDataSource: false,
addScenarioDashBoard: false,
skipScenario: false,
scenario: () => {
e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } });
// Show zoom
e2e.components.OptionsGroup.group('Map controls').scrollIntoView().should('exist');
cy.get('input[id="controls.showZoom"]').check({ force: true });
cy.contains('+');
cy.get('.ol-zoom').should('exist');
// Show attribution
cy.get('input[id="controls.showAttribution"]').check({ force: true });
cy.get('.ol-attribution').should('exist');
// Show scale
cy.get('input[id="controls.showScale"]').check({ force: true });
cy.get('.ol-scale-line').should('exist');
// Show measure tool
cy.get('input[id="controls.showMeasure"]').check({ force: true });
e2e.components.PanelEditor.measureButton().should('exist');
// Show debug
cy.get('input[id="controls.showDebug"]').check({ force: true });
e2e.components.DebugOverlay.wrapper().should('exist');
},
});
@@ -0,0 +1,119 @@
import { e2e } from '@grafana/e2e';
const DASHBOARD_ID = 'P2jR04WVk';
e2e.scenario({
describeName: 'Geomap spatial operations - auto transforms',
itName: 'Tests location auto option',
addScenarioDataSource: false,
addScenarioDashBoard: false,
skipScenario: false,
scenario: () => {
e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } });
e2e.components.Tab.title('Transform').should('be.visible').click();
e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click();
e2e.components.Transforms.SpatialOperations.actionLabel().type('Prepare spatial field{enter}');
e2e.components.Transforms.SpatialOperations.locationLabel().should('be.visible');
e2e.components.Transforms.SpatialOperations.location.autoOption().check({ force: true });
e2e.components.Transforms.SpatialOperations.location.autoOption().should('be.checked');
e2e.components.PanelEditor.toggleTableView().click({ force: true });
e2e.components.Panels.Visualization.Table.header()
.should('be.visible')
.within(() => {
cy.contains('Point').should('be.visible');
});
},
});
e2e.scenario({
describeName: 'Geomap spatial operations - coords transforms ',
itName: 'Tests location coords option',
addScenarioDataSource: false,
addScenarioDashBoard: false,
skipScenario: false,
scenario: () => {
e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } });
e2e.components.Tab.title('Transform').should('be.visible').click();
e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click();
e2e.components.Transforms.SpatialOperations.actionLabel().type('Prepare spatial field{enter}');
e2e.components.Transforms.SpatialOperations.locationLabel().should('be.visible');
e2e.components.Transforms.SpatialOperations.location.coords.option().check({ force: true });
e2e.components.Transforms.SpatialOperations.location.coords.option().should('be.checked');
e2e.components.Transforms.SpatialOperations.location.coords.latitudeFieldLabel().should('be.visible');
e2e.components.Transforms.SpatialOperations.location.coords.latitudeFieldLabel().type('Lat{enter}');
e2e.components.Transforms.SpatialOperations.location.coords.longitudeFieldLabel().should('be.visible');
e2e.components.Transforms.SpatialOperations.location.coords.longitudeFieldLabel().type('Lng{enter}');
e2e.components.PanelEditor.toggleTableView().click({ force: true });
e2e.components.Panels.Visualization.Table.header()
.should('be.visible')
.within(() => {
cy.contains('Point').should('be.visible');
});
},
});
e2e.scenario({
describeName: 'Geomap spatial operations - geohash transforms ',
itName: 'Tests geoshash field column appears in table view',
addScenarioDataSource: false,
addScenarioDashBoard: false,
skipScenario: false,
scenario: () => {
e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } });
e2e.components.Tab.title('Transform').should('be.visible').click();
e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click();
e2e.components.Transforms.SpatialOperations.actionLabel().type('Prepare spatial field{enter}');
e2e.components.Transforms.SpatialOperations.locationLabel().should('be.visible');
e2e.components.Transforms.SpatialOperations.location.geohash.option().check({ force: true });
e2e.components.Transforms.SpatialOperations.location.geohash
.geohashFieldLabel()
.should('be.visible')
.type('State{enter}');
e2e.components.PanelEditor.toggleTableView().click({ force: true });
e2e.components.Panels.Visualization.Table.header()
.should('be.visible')
.within(() => {
cy.contains('State 1').should('be.visible');
});
},
});
e2e.scenario({
describeName: 'Geomap spatial operations - lookup transforms ',
itName: 'Tests location lookup option',
addScenarioDataSource: false,
addScenarioDashBoard: false,
skipScenario: false,
scenario: () => {
e2e.flows.openDashboard({ uid: DASHBOARD_ID, queryParams: { editPanel: 1 } });
e2e.components.Tab.title('Transform').should('be.visible').click();
e2e.components.TransformTab.newTransform('Spatial operations').scrollIntoView().should('be.visible').click();
e2e.components.Transforms.SpatialOperations.actionLabel().type('Prepare spatial field{enter}');
e2e.components.Transforms.SpatialOperations.locationLabel().should('be.visible');
e2e.components.Transforms.SpatialOperations.location.lookup.option().check({ force: true });
e2e.components.Transforms.SpatialOperations.location.lookup.option().should('be.checked');
e2e.components.Transforms.SpatialOperations.location.lookup.lookupFieldLabel().should('be.visible');
e2e.components.Transforms.SpatialOperations.location.lookup.lookupFieldLabel().type('State{enter}');
e2e.components.Transforms.SpatialOperations.location.lookup.gazetteerFieldLabel().should('be.visible');
e2e.components.Transforms.SpatialOperations.location.lookup.gazetteerFieldLabel().type('USA States{enter}');
e2e.components.PanelEditor.toggleTableView().click({ force: true });
e2e.components.Panels.Visualization.Table.header()
.should('be.visible')
.within(() => {
cy.contains('Geometry').should('be.visible');
});
},
});
@@ -127,6 +127,9 @@ export const Components = {
toggleVizPicker: 'toggle-viz-picker',
toggleVizOptions: 'toggle-viz-options',
toggleTableView: 'toggle-table-view',
// [Geomap] Map controls
measureButton: 'show measure tools',
},
PanelInspector: {
Data: {
@@ -199,6 +202,27 @@ export const Components = {
modeLabel: 'Transform mode label',
calculationsLabel: 'Transform calculations label',
},
SpatialOperations: {
actionLabel: 'root Action field property editor',
locationLabel: 'root Location field property editor',
location: {
autoOption: 'Auto location option',
coords: {
option: 'Coords location option',
latitudeFieldLabel: 'root Latitude field field property editor',
longitudeFieldLabel: 'root Longitude field field property editor',
},
geohash: {
option: 'Geohash location option',
geohashFieldLabel: 'root Geohash field field property editor',
},
lookup: {
option: 'Lookup location option',
lookupFieldLabel: 'root Lookup field field property editor',
gazetteerFieldLabel: 'root Gazetteer field property editor',
},
},
},
searchInput: 'search transformations',
},
PageToolbar: {
@@ -347,4 +371,7 @@ export const Components = {
inputField: 'data-testid-file-upload-input-field',
fileNameSpan: 'data-testid-file-upload-file-name',
},
DebugOverlay: {
wrapper: 'debug-overlay',
},
};
+1
View File
@@ -148,6 +148,7 @@ export const getAvailableIcons = () =>
'record-audio',
'repeat',
'rocket',
'ruler-combined',
'save',
'search',
'search-minus',
@@ -458,7 +458,7 @@
"hasUpdate": false,
"defaultNavUrl": "/plugins/geomap/",
"category": "",
"state": "beta",
"state": "",
"signature": "internal",
"signatureType": "",
"signatureOrg": ""
@@ -0,0 +1,33 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { AddLayerButton, AddLayerButtonProps } from './AddLayerButton';
describe('AddLayerButton', () => {
const testLabel = 'Add Layer';
it('renders AddLayerButton', () => {
renderScenario({});
const button = screen.getByLabelText(selectors.components.ValuePicker.button(testLabel));
expect(button).toBeInTheDocument();
});
function renderScenario(overrides: Partial<AddLayerButtonProps>) {
const dummyOptions = [{ description: 'Use markers to render each data point', label: 'Markers', value: 'markers' }];
const props: AddLayerButtonProps = {
onChange: jest.fn(),
options: dummyOptions,
label: testLabel,
};
Object.assign(props, overrides);
return {
props,
renderResult: render(<AddLayerButton {...props} />),
};
}
});
@@ -3,7 +3,7 @@ import React from 'react';
import { SelectableValue } from '@grafana/data';
import { ValuePicker } from '@grafana/ui';
type AddLayerButtonProps = {
export type AddLayerButtonProps = {
onChange: (sel: SelectableValue<string>) => void;
options: Array<SelectableValue<string>>;
label: string;
@@ -0,0 +1,77 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { DATA_TEST_ID, LayerDragDropList, LayerDragDropListProps } from './LayerDragDropList';
describe('LayerDragDropList', () => {
type testLayer = { name: string; getName: () => string };
const layerOneName = 'rocket12345';
const layerTwoName = 'man6789';
it('renders LayerDragDropList', () => {
renderScenario({});
screen.getByTestId(DATA_TEST_ID);
screen.getByText(layerOneName);
screen.getByText(layerTwoName);
});
it('excludeBaseLayer', () => {
renderScenario({ excludeBaseLayer: true });
screen.getByText(layerTwoName);
expect(screen.queryByTestId(layerOneName)).not.toBeInTheDocument();
});
it('showActions', () => {
renderScenario({ showActions: () => true });
expect(screen.getAllByLabelText('Duplicate button').length).toEqual(2);
expect(screen.getAllByLabelText('Remove button').length).toEqual(2);
});
it('showActions - no duplicate', () => {
renderScenario({ showActions: () => true, onDuplicate: undefined });
expect(screen.getAllByLabelText('Remove button').length).toEqual(2);
expect(screen.queryAllByLabelText('Duplicate button').length).toEqual(0);
});
it('renders draggable icon', () => {
renderScenario({});
expect(screen.getAllByLabelText('Drag and drop icon').length).toEqual(2);
});
it('does not render draggable icon', () => {
renderScenario({ excludeBaseLayer: true });
expect(screen.queryAllByLabelText('Drag and drop icon').length).toEqual(0);
});
function renderScenario(overrides: Partial<LayerDragDropListProps<testLayer>>) {
const testLayers = [
{ name: layerOneName, getName: () => layerOneName },
{ name: layerTwoName, getName: () => layerTwoName },
];
const props: LayerDragDropListProps<testLayer> = {
layers: testLayers,
getLayerInfo: jest.fn(),
onDragEnd: jest.fn(),
onSelect: jest.fn(),
onDelete: jest.fn(),
onDuplicate: jest.fn(),
showActions: jest.fn(),
selection: [],
excludeBaseLayer: false,
onNameChange: jest.fn(),
verifyLayerNameUniqueness: undefined,
};
Object.assign(props, overrides);
return {
props,
renderResult: render(<LayerDragDropList {...props} />),
};
}
});
@@ -9,17 +9,19 @@ import { Icon, IconButton, stylesFactory } from '@grafana/ui';
import { LayerName } from './LayerName';
import { LayerElement } from './types';
type LayerDragDropListProps<T extends LayerElement> = {
export const DATA_TEST_ID = 'layer-drag-drop-list';
export type LayerDragDropListProps<T extends LayerElement> = {
layers: T[];
getLayerInfo: (element: T) => string;
onDragEnd: (result: DropResult) => void;
onSelect: (element: T) => any;
onDelete: (element: T) => any;
onDuplicate?: (element: T) => any;
onSelect: (element: T) => void;
onDelete: (element: T) => void;
onDuplicate?: (element: T) => void;
showActions: (element: T) => boolean;
selection?: string[]; // list of unique ids (names)
excludeBaseLayer?: boolean;
onNameChange: (element: T, newName: string) => any;
onNameChange: (element: T, newName: string) => void;
verifyLayerNameUniqueness?: (nameToCheck: string) => boolean;
};
@@ -46,7 +48,7 @@ export const LayerDragDropList = <T extends LayerElement>({
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable">
{(provided, snapshot) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
<div {...provided.droppableProps} ref={provided.innerRef} data-testid={DATA_TEST_ID}>
{(() => {
// reverse order
const rows: any = [];
@@ -80,6 +82,7 @@ export const LayerDragDropList = <T extends LayerElement>({
<IconButton
name="copy"
title={'Duplicate'}
ariaLabel={'Duplicate button'}
className={style.actionIcon}
onClick={() => onDuplicate(element)}
/>
@@ -88,6 +91,7 @@ export const LayerDragDropList = <T extends LayerElement>({
<IconButton
name="trash-alt"
title={'remove'}
ariaLabel={'Remove button'}
className={cx(style.actionIcon, style.dragIcon)}
onClick={() => onDelete(element)}
/>
@@ -95,6 +99,7 @@ export const LayerDragDropList = <T extends LayerElement>({
)}
{layers.length > shouldRenderDragIconLengthThreshold && (
<Icon
aria-label="Drag and drop icon"
title="Drag and drop to reorder"
name="draggabledots"
size="lg"
@@ -5,6 +5,7 @@ import {
FrameGeometrySourceMode,
PanelOptionsEditorBuilder,
} from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors/src';
import { GazetteerPathEditor } from 'app/features/geo/editor/GazetteerPathEditor';
export function addLocationFields<TOptions>(
@@ -20,10 +21,26 @@ export function addLocationFields<TOptions>(
defaultValue: FrameGeometrySourceMode.Auto,
settings: {
options: [
{ value: FrameGeometrySourceMode.Auto, label: 'Auto' },
{ value: FrameGeometrySourceMode.Coords, label: 'Coords' },
{ value: FrameGeometrySourceMode.Geohash, label: 'Geohash' },
{ value: FrameGeometrySourceMode.Lookup, label: 'Lookup' },
{
value: FrameGeometrySourceMode.Auto,
label: 'Auto',
ariaLabel: selectors.components.Transforms.SpatialOperations.location.autoOption,
},
{
value: FrameGeometrySourceMode.Coords,
label: 'Coords',
ariaLabel: selectors.components.Transforms.SpatialOperations.location.coords.option,
},
{
value: FrameGeometrySourceMode.Geohash,
label: 'Geohash',
ariaLabel: selectors.components.Transforms.SpatialOperations.location.geohash.option,
},
{
value: FrameGeometrySourceMode.Lookup,
label: 'Lookup',
ariaLabel: selectors.components.Transforms.SpatialOperations.location.lookup.option,
},
],
},
});
@@ -1,39 +1,33 @@
import { css } from '@emotion/css';
import React, { CSSProperties, PureComponent } from 'react';
import React, { CSSProperties } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { config } from '@grafana/runtime';
import { stylesFactory } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
export interface OverlayProps {
topRight?: React.ReactNode[];
topRight1?: React.ReactNode[];
topRight2?: React.ReactNode[];
bottomLeft?: React.ReactNode[];
blStyle?: CSSProperties;
}
export class GeomapOverlay extends PureComponent<OverlayProps> {
style = getStyles(config.theme);
export const GeomapOverlay = ({ topRight1, topRight2, bottomLeft, blStyle }: OverlayProps) => {
const topRight1Exists = (topRight1 && topRight1.length > 0) ?? false;
const styles = useStyles2(getStyles(topRight1Exists));
return (
<div className={styles.overlay}>
{Boolean(topRight1?.length) && <div className={styles.TR1}>{topRight1}</div>}
{Boolean(topRight2?.length) && <div className={styles.TR2}>{topRight2}</div>}
{Boolean(bottomLeft?.length) && (
<div className={styles.BL} style={blStyle}>
{bottomLeft}
</div>
)}
</div>
);
};
constructor(props: OverlayProps) {
super(props);
}
render() {
const { topRight, bottomLeft } = this.props;
return (
<div className={this.style.overlay}>
{Boolean(topRight?.length) && <div className={this.style.TR}>{topRight}</div>}
{Boolean(bottomLeft?.length) && (
<div className={this.style.BL} style={this.props.blStyle}>
{bottomLeft}
</div>
)}
</div>
);
}
}
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
const getStyles = (topRight1Exists: boolean) => (theme: GrafanaTheme2) => ({
overlay: css`
position: absolute;
width: 100%;
@@ -41,9 +35,15 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
z-index: 500;
pointer-events: none;
`,
TR: css`
TR1: css`
right: 0.5em;
pointer-events: auto;
position: absolute;
top: 8px;
top: 0.5em;
`,
TR2: css`
position: absolute;
top: ${topRight1Exists ? '80' : '8'}px;
right: 8px;
pointer-events: auto;
`,
@@ -53,4 +53,4 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
left: 8px;
pointer-events: auto;
`,
}));
});
+68 -444
View File
@@ -1,32 +1,14 @@
import { css } from '@emotion/css';
import { Global } from '@emotion/react';
import { cloneDeep } from 'lodash';
import { Collection, Map as OpenLayersMap, MapBrowserEvent, PluggableMap, View } from 'ol';
import { FeatureLike } from 'ol/Feature';
import { Map as OpenLayersMap, MapBrowserEvent, View } from 'ol';
import Attribution from 'ol/control/Attribution';
import ScaleLine from 'ol/control/ScaleLine';
import Zoom from 'ol/control/Zoom';
import { Coordinate } from 'ol/coordinate';
import { isEmpty } from 'ol/extent';
import { defaults as interactionDefaults } from 'ol/interaction';
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
import BaseLayer from 'ol/layer/Base';
import { fromLonLat, toLonLat } from 'ol/proj';
import React, { Component, ReactNode } from 'react';
import { Subject, Subscription } from 'rxjs';
import { Subscription } from 'rxjs';
import {
DataFrame,
DataHoverClearEvent,
DataHoverEvent,
FrameGeometrySourceMode,
getFrameMatchers,
GrafanaTheme,
MapLayerHandler,
MapLayerOptions,
PanelData,
PanelProps,
} from '@grafana/data';
import { DataHoverEvent, GrafanaTheme, PanelData, PanelProps } from '@grafana/data';
import { config } from '@grafana/runtime';
import { PanelContext, PanelContextRoot, stylesFactory } from '@grafana/ui';
import { PanelEditExitedEvent } from 'app/types/events';
@@ -34,13 +16,18 @@ import { PanelEditExitedEvent } from 'app/types/events';
import { GeomapOverlay, OverlayProps } from './GeomapOverlay';
import { GeomapTooltip } from './GeomapTooltip';
import { DebugOverlay } from './components/DebugOverlay';
import { GeomapHoverPayload, GeomapLayerHover } from './event';
import { MeasureOverlay } from './components/MeasureOverlay';
import { MeasureVectorLayer } from './components/MeasureVectorLayer';
import { GeomapHoverPayload } from './event';
import { getGlobalStyles } from './globalStyles';
import { defaultMarkersConfig, MARKERS_LAYER_ID } from './layers/data/markersLayer';
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from './layers/registry';
import { ControlsOptions, GeomapPanelOptions, MapLayerState, MapViewConfig, TooltipMode } from './types';
import { getLayersExtent } from './utils/getLayersExtent';
import { centerPointRegistry, MapCenterID } from './view';
import { defaultMarkersConfig } from './layers/data/markersLayer';
import { DEFAULT_BASEMAP_CONFIG } from './layers/registry';
import { ControlsOptions, GeomapPanelOptions, MapLayerState, TooltipMode } from './types';
import { getActions } from './utils/actions';
import { applyLayerFilter, initLayer } from './utils/layers';
import { pointerClickListener, pointerMoveListener, setTooltipListeners } from './utils/tootltip';
import { updateMap, getNewOpenLayersMap, notifyPanelEditor } from './utils/utils';
import { initMapView, initViewExtent } from './utils/view';
// Allows multiple panels to share the same view instance
let sharedView: View | undefined = undefined;
@@ -50,26 +37,12 @@ interface State extends OverlayProps {
ttip?: GeomapHoverPayload;
ttipOpen: boolean;
legends: ReactNode[];
}
export interface GeomapLayerActions {
selectLayer: (uid: string) => void;
deleteLayer: (uid: string) => void;
addlayer: (type: string) => void;
reorder: (src: number, dst: number) => void;
canRename: (v: string) => boolean;
}
export interface GeomapInstanceState {
map?: OpenLayersMap;
layers: MapLayerState[];
selected: number;
actions: GeomapLayerActions;
measureMenuActive?: boolean;
}
export class GeomapPanel extends Component<Props, State> {
static contextType = PanelContextRoot;
panelContext: PanelContext = {} as PanelContext;
panelContext: PanelContext | undefined = undefined;
private subs = new Subscription();
globalCSS = getGlobalStyles(config.theme2);
@@ -97,7 +70,7 @@ export class GeomapPanel extends Component<Props, State> {
}
componentDidMount() {
this.panelContext = this.context as PanelContext;
this.panelContext = this.context;
}
componentWillUnmount() {
@@ -141,103 +114,26 @@ export class GeomapPanel extends Component<Props, State> {
}
/** This function will actually update the JSON model */
private doOptionsUpdate(selected: number) {
doOptionsUpdate(selected: number) {
const { options, onOptionsChange } = this.props;
const layers = this.layers;
this.map?.getLayers().forEach((l) => {
if (l instanceof MeasureVectorLayer) {
this.map?.removeLayer(l);
this.map?.addLayer(l);
}
});
onOptionsChange({
...options,
basemap: layers[0].options,
layers: layers.slice(1).map((v) => v.options),
});
// Notify the panel editor
if (this.panelContext.onInstanceStateChange) {
this.panelContext.onInstanceStateChange({
map: this.map,
layers: layers,
selected,
actions: this.actions,
});
}
notifyPanelEditor(this, layers, selected);
this.setState({ legends: this.getLegends() });
}
getNextLayerName = () => {
let idx = this.layers.length; // since basemap is 0, this looks right
while (true && idx < 100) {
const name = `Layer ${idx++}`;
if (!this.byName.has(name)) {
return name;
}
}
return `Layer ${Date.now()}`;
};
actions: GeomapLayerActions = {
selectLayer: (uid: string) => {
const selected = this.layers.findIndex((v) => v.options.name === uid);
if (this.panelContext.onInstanceStateChange) {
this.panelContext.onInstanceStateChange({
map: this.map,
layers: this.layers,
selected,
actions: this.actions,
});
}
},
canRename: (v: string) => {
return !this.byName.has(v);
},
deleteLayer: (uid: string) => {
const layers: MapLayerState[] = [];
for (const lyr of this.layers) {
if (lyr.options.name === uid) {
this.map?.removeLayer(lyr.layer);
} else {
layers.push(lyr);
}
}
this.layers = layers;
this.doOptionsUpdate(0);
},
addlayer: (type: string) => {
const item = geomapLayerRegistry.getIfExists(type);
if (!item) {
return; // ignore empty request
}
this.initLayer(
this.map!,
{
type: item.id,
name: this.getNextLayerName(),
config: cloneDeep(item.defaultOptions),
location: item.showLocation ? { mode: FrameGeometrySourceMode.Auto } : undefined,
tooltip: true,
},
false
).then((lyr) => {
this.layers = this.layers.slice(0);
this.layers.push(lyr);
this.map?.addLayer(lyr.layer);
this.doOptionsUpdate(this.layers.length - 1);
});
},
reorder: (startIndex: number, endIndex: number) => {
const result = Array.from(this.layers);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
this.layers = result;
this.doOptionsUpdate(endIndex);
// Add the layers in the right order
const group = this.map?.getLayers()!;
group.clear();
this.layers.forEach((v) => group.push(v.layer));
},
};
actions = getActions(this);
/**
* Called when the panel options change
@@ -246,15 +142,14 @@ export class GeomapPanel extends Component<Props, State> {
*/
optionsChanged(options: GeomapPanelOptions) {
const oldOptions = this.props.options;
console.log('options changed!', options);
if (options.view !== oldOptions.view) {
console.log('View changed');
this.map!.setView(this.initMapView(options.view, this.map!.getLayers()));
const [updatedSharedView, view] = initMapView(options.view, sharedView, this.map!.getLayers());
sharedView = updatedSharedView;
// eslint-disable-next-line
this.map!.setView(view as View);
}
if (options.controls !== oldOptions.controls) {
console.log('Controls changed');
this.initControls(options.controls ?? { showZoom: true, showAttribution: true });
}
}
@@ -266,7 +161,7 @@ export class GeomapPanel extends Component<Props, State> {
// Only update if panel data matches component data
if (data === this.props.data) {
for (const state of this.layers) {
this.applyLayerFilter(state.handler, state.options);
applyLayerFilter(state.handler, state.options, this.props.data);
}
}
}
@@ -278,35 +173,27 @@ export class GeomapPanel extends Component<Props, State> {
}
if (!div) {
// eslint-disable-next-line
this.map = undefined as unknown as OpenLayersMap;
return;
}
const { options } = this.props;
const map = (this.map = new OpenLayersMap({
view: this.initMapView(options.view, undefined),
pixelRatio: 1, // or zoom?
layers: [], // loaded explicitly below
controls: [],
target: div,
interactions: interactionDefaults({
mouseWheelZoom: false, // managed by initControls
}),
}));
const map = getNewOpenLayersMap(this, options, div);
this.byName.clear();
const layers: MapLayerState[] = [];
try {
layers.push(await this.initLayer(map, options.basemap ?? DEFAULT_BASEMAP_CONFIG, true));
layers.push(await initLayer(this, map, options.basemap ?? DEFAULT_BASEMAP_CONFIG, true));
// Default layer values
const layerOptions = options.layers ?? [defaultMarkersConfig];
for (const lyr of layerOptions) {
layers.push(await this.initLayer(map, lyr, false));
layers.push(await initLayer(this, map, lyr, false));
}
} catch (ex) {
console.error('error loading layers', ex);
console.error('error loading layers', ex); // eslint-disable-line no-console
}
for (const lyr of layers) {
@@ -314,29 +201,14 @@ export class GeomapPanel extends Component<Props, State> {
}
this.layers = layers;
this.map = map; // redundant
this.initViewExtent(map.getView(), options.view, map.getLayers());
initViewExtent(map.getView(), options.view, map.getLayers());
this.mouseWheelZoom = new MouseWheelZoom();
this.map.addInteraction(this.mouseWheelZoom);
this.initControls(options.controls);
this.forceUpdate(); // first render
this.map?.addInteraction(this.mouseWheelZoom);
// Tooltip listener
this.map.on('singleclick', this.pointerClickListener);
this.map.on('pointermove', this.pointerMoveListener);
this.map.getViewport().addEventListener('mouseout', (evt) => {
this.props.eventBus.publish(new DataHoverClearEvent());
});
// Notify the panel editor
if (this.panelContext.onInstanceStateChange) {
this.panelContext.onInstanceStateChange({
map: this.map,
layers: layers,
selected: layers.length - 1, // the top layer
actions: this.actions,
});
}
updateMap(this, options);
setTooltipListeners(this);
notifyPanelEditor(this, layers, layers.length - 1);
this.setState({ legends: this.getLegends() });
};
@@ -352,280 +224,13 @@ export class GeomapPanel extends Component<Props, State> {
};
pointerClickListener = (evt: MapBrowserEvent<UIEvent>) => {
if (this.pointerMoveListener(evt)) {
evt.preventDefault();
evt.stopPropagation();
this.mapDiv!.style.cursor = 'auto';
this.setState({ ttipOpen: true });
}
pointerClickListener(evt, this);
};
pointerMoveListener = (evt: MapBrowserEvent<UIEvent>) => {
if (!this.map || this.state.ttipOpen) {
return false;
}
const mouse = evt.originalEvent as any;
const pixel = this.map.getEventPixel(mouse);
const hover = toLonLat(this.map.getCoordinateFromPixel(pixel));
const { hoverPayload } = this;
hoverPayload.pageX = mouse.pageX;
hoverPayload.pageY = mouse.pageY;
hoverPayload.point = {
lat: hover[1],
lon: hover[0],
};
hoverPayload.data = undefined;
hoverPayload.columnIndex = undefined;
hoverPayload.rowIndex = undefined;
hoverPayload.layers = undefined;
const layers: GeomapLayerHover[] = [];
const layerLookup = new Map<MapLayerState, GeomapLayerHover>();
let ttip: GeomapHoverPayload = {} as GeomapHoverPayload;
this.map.forEachFeatureAtPixel(
pixel,
(feature, layer, geo) => {
const s: MapLayerState = (layer as any).__state;
//match hover layer to layer in layers
//check if the layer show tooltip is enabled
//then also pass the list of tooltip fields if exists
//this is used as the generic hover event
if (!hoverPayload.data) {
const props = feature.getProperties();
const frame = props['frame'];
if (frame) {
hoverPayload.data = ttip.data = frame as DataFrame;
hoverPayload.rowIndex = ttip.rowIndex = props['rowIndex'];
}
if (s?.mouseEvents) {
s.mouseEvents.next(feature);
}
}
if (s) {
let h = layerLookup.get(s);
if (!h) {
h = { layer: s, features: [] };
layerLookup.set(s, h);
layers.push(h);
}
h.features.push(feature);
}
},
{
layerFilter: (l) => {
const hoverLayerState = (l as any).__state as MapLayerState;
return hoverLayerState?.options?.tooltip !== false;
},
}
);
this.hoverPayload.layers = layers.length ? layers : undefined;
this.props.eventBus.publish(this.hoverEvent);
this.setState({ ttip: { ...hoverPayload } });
if (!layers.length) {
// clear mouse events
this.layers.forEach((layer) => {
layer.mouseEvents.next(undefined);
});
}
const found = layers.length ? true : false;
this.mapDiv!.style.cursor = found ? 'pointer' : 'auto';
return found;
pointerMoveListener(evt, this);
};
private updateLayer = async (uid: string, newOptions: MapLayerOptions): Promise<boolean> => {
if (!this.map) {
return false;
}
const current = this.byName.get(uid);
if (!current) {
return false;
}
let layerIndex = -1;
const group = this.map?.getLayers()!;
for (let i = 0; i < group?.getLength(); i++) {
if (group.item(i) === current.layer) {
layerIndex = i;
break;
}
}
// Special handling for rename
if (newOptions.name !== uid) {
if (!newOptions.name) {
newOptions.name = uid;
} else if (this.byName.has(newOptions.name)) {
return false;
}
console.log('Layer name changed', uid, '>>>', newOptions.name);
this.byName.delete(uid);
uid = newOptions.name;
this.byName.set(uid, current);
}
// Type changed -- requires full re-initalization
if (current.options.type !== newOptions.type) {
// full init
} else {
// just update options
}
const layers = this.layers.slice(0);
try {
const info = await this.initLayer(this.map, newOptions, current.isBasemap);
layers[layerIndex]?.handler.dispose?.();
layers[layerIndex] = info;
group.setAt(layerIndex, info.layer);
// initialize with new data
this.applyLayerFilter(info.handler, newOptions);
} catch (err) {
console.warn('ERROR', err);
return false;
}
// Just to trigger a state update
this.setState({ legends: [] });
this.layers = layers;
this.doOptionsUpdate(layerIndex);
return true;
};
async initLayer(map: PluggableMap, options: MapLayerOptions, isBasemap?: boolean): Promise<MapLayerState> {
if (isBasemap && (!options?.type || config.geomapDisableCustomBaseLayer)) {
options = DEFAULT_BASEMAP_CONFIG;
}
// Use default makers layer
if (!options?.type) {
options = {
type: MARKERS_LAYER_ID,
name: this.getNextLayerName(),
config: {},
};
}
const item = geomapLayerRegistry.getIfExists(options.type);
if (!item) {
return Promise.reject('unknown layer: ' + options.type);
}
const handler = await item.create(map, options, this.props.eventBus, config.theme2);
const layer = handler.init();
if (options.opacity != null) {
layer.setOpacity(options.opacity);
}
if (!options.name) {
options.name = this.getNextLayerName();
}
const UID = options.name;
const state: MapLayerState<any> = {
// UID, // unique name when added to the map (it may change and will need special handling)
isBasemap,
options,
layer,
handler,
mouseEvents: new Subject<FeatureLike | undefined>(),
getName: () => UID,
// Used by the editors
onChange: (cfg: MapLayerOptions) => {
this.updateLayer(UID, cfg);
},
};
this.byName.set(UID, state);
(state.layer as any).__state = state;
this.applyLayerFilter(handler, options);
return state;
}
applyLayerFilter(handler: MapLayerHandler<any>, options: MapLayerOptions<any>): void {
if (handler.update) {
let panelData = this.props.data;
if (options.filterData) {
const matcherFunc = getFrameMatchers(options.filterData);
panelData = {
...panelData,
series: panelData.series.filter(matcherFunc),
};
}
handler.update(panelData);
}
}
initMapView(config: MapViewConfig, layers?: Collection<BaseLayer>): View {
let view = new View({
center: [0, 0],
zoom: 1,
showFullExtent: true, // allows zooming so the full range is visible
});
// With shared views, all panels use the same view instance
if (config.shared) {
if (!sharedView) {
sharedView = view;
} else {
view = sharedView;
}
}
if (layers) {
this.initViewExtent(view, config, layers);
}
return view;
}
initViewExtent(view: View, config: MapViewConfig, layers: Collection<BaseLayer>) {
const v = centerPointRegistry.getIfExists(config.id);
if (v) {
let coord: Coordinate | undefined = undefined;
if (v.lat == null) {
if (v.id === MapCenterID.Coordinates) {
coord = [config.lon ?? 0, config.lat ?? 0];
} else if (v.id === MapCenterID.Fit) {
const extent = getLayersExtent(layers);
if (!isEmpty(extent)) {
view.fit(extent, {
padding: [30, 30, 30, 30],
maxZoom: config.zoom ?? config.maxZoom,
});
}
} else {
console.log('TODO, view requires special handling', v);
}
} else {
coord = [v.lon ?? 0, v.lat ?? 0];
}
if (coord) {
view.setCenter(fromLonLat(coord));
}
}
if (config.maxZoom) {
view.setMaxZoom(config.maxZoom);
}
if (config.minZoom) {
view.setMaxZoom(config.minZoom);
}
if (config.zoom && v?.id !== MapCenterID.Fit) {
view.setZoom(config.zoom);
}
}
initControls(options: ControlsOptions) {
if (!this.map) {
return;
@@ -652,12 +257,26 @@ export class GeomapPanel extends Component<Props, State> {
}
// Update the react overlays
let topRight: ReactNode[] = [];
if (options.showDebug) {
topRight = [<DebugOverlay key="debug" map={this.map} />];
let topRight1: ReactNode[] = [];
if (options.showMeasure) {
topRight1 = [
<MeasureOverlay
key="measure"
map={this.map}
// Lifts menuActive state and resets tooltip state upon close
menuActiveState={(value: boolean) => {
this.setState({ ttipOpen: value, measureMenuActive: value });
}}
/>,
];
}
this.setState({ topRight });
let topRight2: ReactNode[] = [];
if (options.showDebug) {
topRight2 = [<DebugOverlay key="debug" map={this.map} />];
}
this.setState({ topRight1, topRight2 });
}
getLegends() {
@@ -672,7 +291,7 @@ export class GeomapPanel extends Component<Props, State> {
}
render() {
let { ttip, ttipOpen, topRight, legends } = this.state;
let { ttip, ttipOpen, topRight1, legends, topRight2 } = this.state;
const { options } = this.props;
const showScale = options.controls.showScale;
if (!ttipOpen && options.tooltip?.mode === TooltipMode.None) {
@@ -684,7 +303,12 @@ export class GeomapPanel extends Component<Props, State> {
<Global styles={this.globalCSS} />
<div className={this.style.wrap} onMouseLeave={this.clearTooltip}>
<div className={this.style.map} ref={this.initMapRef}></div>
<GeomapOverlay bottomLeft={legends} topRight={topRight} blStyle={{ bottom: showScale ? '35px' : '8px' }} />
<GeomapOverlay
bottomLeft={legends}
topRight1={topRight1}
topRight2={topRight2}
blStyle={{ bottom: showScale ? '35px' : '8px' }}
/>
</div>
<GeomapTooltip ttip={ttip} isOpen={ttipOpen} onClose={this.tooltipPopupClosed} />
</>
@@ -63,7 +63,7 @@ export const generateLabel = (feature: FeatureLike, idx: number): string => {
const names = ['Name', 'name', 'Title', 'ID', 'id'];
let props = feature.getProperties();
let first = '';
const frame = feature.get('frame') as DataFrame;
const frame = feature.get('frame') as DataFrame; // eslint-disable-line
if (frame) {
const rowIndex = feature.get('rowIndex');
for (const f of frame.fields) {
@@ -34,7 +34,7 @@ export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder, mode }:
return null;
}
const displayValues: Array<[string, any, string]> = [];
const displayValues: Array<[string, unknown, string]> = [];
const links: Array<LinkModel<Field>> = [];
const linkLookup = new Set<string>();
@@ -6,6 +6,7 @@ import React, { PureComponent } from 'react';
import tinycolor from 'tinycolor2';
import { GrafanaTheme } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors/src';
import { stylesFactory } from '@grafana/ui';
import { config } from 'app/core/config';
@@ -43,7 +44,7 @@ export class DebugOverlay extends PureComponent<Props, State> {
const { zoom, center } = this.state;
return (
<div className={this.style.infoWrap}>
<div className={this.style.infoWrap} aria-label={selectors.components.DebugOverlay.wrapper}>
<table>
<tbody>
<tr>
@@ -36,13 +36,13 @@ export function MarkersLegend(props: MarkersLegendProps) {
}
const props = hoverEvent.getProperties();
const frame = props.frame as DataFrame;
const frame = props.frame as DataFrame; // eslint-disable-line
if (!frame) {
return undefined;
}
const rowIndex = props.rowIndex as number;
const rowIndex = props.rowIndex as number; // eslint-disable-line
return colorField.values.get(rowIndex);
}, [hoverEvent, colorField]);
@@ -0,0 +1,140 @@
import { css } from '@emotion/css';
import Map from 'ol/Map';
import React, { useMemo, useRef, useState } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { Button, IconButton, RadioButtonGroup, Select, stylesFactory } from '@grafana/ui';
import { config } from 'app/core/config';
import { MapMeasure, MapMeasureOptions, measures } from '../utils/measure';
import { MeasureVectorLayer } from './MeasureVectorLayer';
type Props = {
map: Map;
menuActiveState: (value: boolean) => void;
};
export const MeasureOverlay = ({ map, menuActiveState }: Props) => {
const vector = useRef(new MeasureVectorLayer());
const measureStyle = getStyles(config.theme2);
// Menu State Management
const [firstLoad, setFirstLoad] = useState<boolean>(true);
const [menuActive, setMenuActive] = useState<boolean>(false);
// Options State
const [options, setOptions] = useState<MapMeasureOptions>({
action: measures[0].value!,
unit: measures[0].units[0].value!,
});
const unit = useMemo(() => {
const action = measures.find((m: MapMeasure) => m.value === options.action) ?? measures[0];
const current = action.getUnit(options.unit);
vector.current.setOptions(options);
return {
current,
options: action.units,
};
}, [options]);
const clearPrevious = true;
const showSegments = false;
function toggleMenu() {
setMenuActive(!menuActive);
// Lift menu state
// TODO: consolidate into one state
menuActiveState(!menuActive);
if (menuActive) {
map.removeInteraction(vector.current.draw);
vector.current.setVisible(false);
} else {
if (firstLoad) {
// Initialize on first load
setFirstLoad(false);
vector.current.setZIndex(1);
map.addLayer(vector.current);
map.addInteraction(vector.current.modify);
}
vector.current.setVisible(true);
map.removeInteraction(vector.current.draw); // Remove last interaction
const a = measures.find((v: MapMeasure) => v.value === options.action) ?? measures[0];
vector.current.addInteraction(map, a.geometry, showSegments, clearPrevious);
}
}
return (
<div className={`${measureStyle.infoWrap} ${!menuActive ? measureStyle.infoWrapClosed : null}`}>
{menuActive ? (
<div>
<div className={measureStyle.rowGroup}>
<RadioButtonGroup
value={options.action}
options={measures}
size="md"
fullWidth={false}
onChange={(e: string) => {
map.removeInteraction(vector.current.draw);
const m = measures.find((v: MapMeasure) => v.value === e) ?? measures[0];
const unit = m.getUnit(options.unit);
setOptions({ ...options, action: m.value!, unit: unit.value! });
vector.current.addInteraction(map, m.geometry, showSegments, clearPrevious);
}}
/>
<Button className={measureStyle.button} icon="times" variant="secondary" size="sm" onClick={toggleMenu} />
</div>
<Select
className={measureStyle.unitSelect}
value={unit.current}
options={unit.options}
isSearchable={false}
onChange={(v: SelectableValue<string>) => {
const a = measures.find((v: SelectableValue<string>) => v.value === options.action) ?? measures[0];
const unit = a.getUnit(v.value) ?? a.units[0];
setOptions({ ...options, unit: unit.value! });
}}
/>
</div>
) : (
<IconButton
className={measureStyle.icon}
name="ruler-combined"
tooltip="show measure tools"
tooltipPlacement="left"
onClick={toggleMenu}
/>
)}
</div>
);
};
const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
button: css`
margin-left: auto;
`,
icon: css`
background-color: ${theme.colors.secondary.main};
display: inline-block;
height: 19.25px;
margin: 1px;
width: 19.25px;
`,
infoWrap: css`
color: ${theme.colors.text};
background-color: ${theme.colors.background.secondary};
border-radius: 4px;
padding: 2px;
`,
infoWrapClosed: css`
height: 25.25px;
width: 25.25px;
`,
rowGroup: css`
display: flex;
justify-content: flex-end;
`,
unitSelect: css`
min-width: 200px;
`,
}));
@@ -0,0 +1,255 @@
import { FeatureLike } from 'ol/Feature';
import Map from 'ol/Map';
import { Coordinate } from 'ol/coordinate';
import { Geometry, LineString, Point, Polygon } from 'ol/geom';
import { Type } from 'ol/geom/Geometry';
import { Draw, Modify } from 'ol/interaction';
import VectorLayer from 'ol/layer/Vector';
import VectorSource from 'ol/source/Vector';
import { getArea, getLength } from 'ol/sphere';
import { Circle as CircleStyle, Fill, RegularShape, Stroke, Style, Text } from 'ol/style';
import { formattedValueToString } from '@grafana/data';
import { MapMeasureOptions, measures } from '../utils/measure';
export class MeasureVectorLayer extends VectorLayer<VectorSource> {
opts: MapMeasureOptions = {
action: 'length',
unit: 'm',
};
constructor() {
super({
source: new VectorSource(),
});
this.setStyle((feature) => {
return this.styleFunction(feature, false);
});
this.setVisible(true);
}
setOptions(options: MapMeasureOptions) {
this.opts = options;
this.getSource()?.refresh();
}
getMapMeasurement(geo: Geometry): string {
let v = 0;
let action = measures[0];
if (this.opts.action === 'area') {
action = measures[1];
v = getArea(geo);
} else {
v = getLength(geo);
}
return formattedValueToString(action.getUnit(this.opts.unit).format(v));
}
segmentStyle = new Style({
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({
color: 'rgba(255, 255, 255, 1)',
}),
backgroundFill: new Fill({
color: 'rgba(0, 0, 0, 0.4)',
}),
padding: [2, 2, 2, 2],
textBaseline: 'bottom',
offsetY: -12,
}),
image: new RegularShape({
radius: 6,
points: 3,
angle: Math.PI,
displacement: [0, 8],
fill: new Fill({
color: 'rgba(0, 0, 0, 0.4)',
}),
}),
});
segmentStyles = [this.segmentStyle];
// Open Layer styles
shapeStyle = [
new Style({
fill: new Fill({
color: 'rgba(255, 255, 255, 0.2)',
}),
image: new CircleStyle({
radius: 5,
stroke: new Stroke({
color: 'rgba(0, 0, 0, 0.7)',
}),
fill: new Fill({
color: 'rgba(255, 255, 255, 0.2)',
}),
}),
}),
new Style({
stroke: new Stroke({
color: [0, 0, 0, 1],
width: 2,
lineDash: [4, 8],
lineDashOffset: 6,
}),
}),
new Style({
stroke: new Stroke({
color: [255, 255, 255, 1],
width: 2,
lineDash: [4, 8],
}),
}),
];
labelStyle = new Style({
text: new Text({
font: '14px Calibri,sans-serif',
fill: new Fill({
color: 'rgba(255, 255, 255, 1)',
}),
backgroundFill: new Fill({
color: 'rgba(0, 0, 0, 0.7)',
}),
padding: [3, 3, 3, 3],
textBaseline: 'bottom',
offsetY: -15,
}),
image: new RegularShape({
radius: 8,
points: 3,
angle: Math.PI,
displacement: [0, 10],
fill: new Fill({
color: 'rgba(0, 0, 0, 0.7)',
}),
}),
});
tipStyle = new Style({
text: new Text({
font: '12px Calibri,sans-serif',
fill: new Fill({
color: 'rgba(255, 255, 255, 1)',
}),
backgroundFill: new Fill({
color: 'rgba(0, 0, 0, 0.4)',
}),
padding: [2, 2, 2, 2],
textAlign: 'left',
offsetX: 15,
}),
});
modifyStyle = new Style({
image: new CircleStyle({
radius: 5,
stroke: new Stroke({
color: 'rgba(0, 0, 0, 0.7)',
}),
fill: new Fill({
color: 'rgba(0, 0, 0, 0.4)',
}),
}),
text: new Text({
text: 'Drag to modify',
font: '12px Calibri,sans-serif',
fill: new Fill({
color: 'rgba(255, 255, 255, 1)',
}),
backgroundFill: new Fill({
color: 'rgba(0, 0, 0, 0.7)',
}),
padding: [2, 2, 2, 2],
textAlign: 'left',
offsetX: 15,
}),
});
readonly modify = new Modify({ source: this.getSource()!, style: this.modifyStyle });
tipPoint!: Geometry;
draw!: Draw; // global so we can remove it later
styleFunction(feature: FeatureLike, segments: boolean, drawType?: string, tip?: string): Style[] {
const styles = [...this.shapeStyle];
const geometry = feature.getGeometry() as Geometry;
if (geometry) {
const type = geometry.getType();
let point: Point;
let label: string;
let line: LineString;
if (!drawType || drawType === type) {
if (type === 'Polygon') {
const poly = geometry as Polygon;
point = poly.getInteriorPoint();
label = this.getMapMeasurement(geometry);
line = new LineString(poly.getCoordinates()[0]);
} else if (type === 'LineString') {
line = geometry as LineString;
point = new Point(line.getLastCoordinate());
label = this.getMapMeasurement(geometry);
}
}
if (segments && line!) {
let count = 0;
line.forEachSegment((a: Coordinate, b: Coordinate) => {
const segment = new LineString([a, b]);
const label = this.getMapMeasurement(segment);
if (this.segmentStyles.length - 1 < count) {
this.segmentStyles.push(this.segmentStyle.clone());
}
const segmentPoint = new Point(segment.getCoordinateAt(0.5));
this.segmentStyles[count].setGeometry(segmentPoint);
this.segmentStyles[count].getText().setText(label);
styles.push(this.segmentStyles[count]);
count++;
});
}
if (label!) {
this.labelStyle.setGeometry(point!);
this.labelStyle.getText().setText(label);
styles.push(this.labelStyle);
}
if (tip && type === 'Point' && !this.modify.getOverlay().getSource().getFeatures().length) {
this.tipPoint = geometry;
this.tipStyle.getText().setText(tip);
styles.push(this.tipStyle);
}
}
return styles;
}
addInteraction(map: Map, typeSelect: Type, showSegments: boolean, clearPrevious: boolean) {
const drawType = typeSelect;
const activeTip =
' Click to continue ' + (drawType === 'Polygon' ? 'polygon' : 'line') + ' \n (double-click to end) ';
const idleTip = ' Click to start ';
let tip = idleTip;
this.draw = new Draw({
source: this.getSource()!,
type: drawType,
style: (feature) => {
return this.styleFunction(feature, showSegments, drawType, tip);
},
});
this.draw.on('drawstart', () => {
if (clearPrevious) {
this.getSource()!.clear();
}
this.modify.setActive(false);
tip = activeTip;
});
this.draw.on('drawend', () => {
this.modifyStyle.setGeometry(this.tipPoint);
this.modify.setActive(true);
map.once('pointermove', () => {
this.modifyStyle.setGeometry('');
});
tip = idleTip;
});
this.modify.setActive(true);
map.addInteraction(this.draw);
}
}
@@ -11,7 +11,7 @@ interface State<T> {
subProps: T;
}
export class ObservablePropsWrapper<T> extends Component<Props<T>, State<T>> {
export class ObservablePropsWrapper<T extends {}> extends Component<Props<T>, State<T>> {
sub?: Unsubscribable;
constructor(props: Props<T>) {
@@ -24,15 +24,10 @@ export class ObservablePropsWrapper<T> extends Component<Props<T>, State<T>> {
componentDidMount() {
this.sub = this.props.watch.subscribe({
next: (subProps: T) => {
//console.log('ObservablePropsWrapper:NEXT', subProps);
this.setState({ subProps });
},
complete: () => {
//console.log('ObservablePropsWrapper:complete');
},
error: (err) => {
//console.log('ObservablePropsWrapper:error', err);
},
complete: () => {},
error: (err) => {},
});
}
@@ -9,7 +9,7 @@ import { FeatureStyleConfig } from '../types';
import { StyleRuleEditor, StyleRuleEditorSettings } from './StyleRuleEditor';
export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[], any, any>> = (props) => {
export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[], unknown, unknown>> = (props) => {
const { value, onChange, context, item } = props;
const theme = useTheme2();
@@ -44,7 +44,7 @@ export const GeomapStyleRulesEditor: FC<StandardEditorProps<FeatureStyleConfig[]
value.map((style, idx: number) => {
const itemSettings: StandardEditorsRegistryItem<any, StyleRuleEditorSettings> = {
settings,
} as any;
} as StandardEditorsRegistryItem;
return (
<StyleRuleEditor
@@ -6,11 +6,10 @@ import { Container } from '@grafana/ui';
import { AddLayerButton } from 'app/core/components/Layers/AddLayerButton';
import { LayerDragDropList } from 'app/core/components/Layers/LayerDragDropList';
import { GeomapInstanceState } from '../GeomapPanel';
import { getLayersOptions } from '../layers/registry';
import { GeomapPanelOptions, MapLayerState } from '../types';
import { GeomapPanelOptions, MapLayerState, GeomapInstanceState } from '../types';
type LayersEditorProps = StandardEditorProps<any, any, GeomapPanelOptions, GeomapInstanceState>;
type LayersEditorProps = StandardEditorProps<unknown, unknown, GeomapPanelOptions, GeomapInstanceState>;
export const LayersEditor = (props: LayersEditorProps) => {
const { layers, selected, actions } = props.context.instanceState ?? {};
@@ -36,19 +35,19 @@ export const LayersEditor = (props: LayersEditorProps) => {
actions.reorder(src, dst);
};
const onSelect = (element: MapLayerState<any>) => {
const onSelect = (element: MapLayerState<unknown>) => {
actions.selectLayer(element.options.name);
};
const onDelete = (element: MapLayerState<any>) => {
const onDelete = (element: MapLayerState<unknown>) => {
actions.deleteLayer(element.options.name);
};
const getLayerInfo = (element: MapLayerState<any>) => {
const getLayerInfo = (element: MapLayerState<unknown>) => {
return element.options.type;
};
const onNameChange = (element: MapLayerState<any>, name: string) => {
const onNameChange = (element: MapLayerState<unknown>, name: string) => {
element.onChange({ ...element.options, name });
};
@@ -5,15 +5,12 @@ import { StandardEditorProps, SelectableValue } from '@grafana/data';
import { Button, InlineField, InlineFieldRow, Select, VerticalGroup } from '@grafana/ui';
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
import { GeomapInstanceState } from '../GeomapPanel';
import { GeomapPanelOptions, MapViewConfig } from '../types';
import { GeomapPanelOptions, MapViewConfig, GeomapInstanceState } from '../types';
import { centerPointRegistry, MapCenterID } from '../view';
export const MapViewEditor: FC<StandardEditorProps<MapViewConfig, any, GeomapPanelOptions, GeomapInstanceState>> = ({
value,
onChange,
context,
}) => {
export const MapViewEditor: FC<
StandardEditorProps<MapViewConfig, unknown, GeomapPanelOptions, GeomapInstanceState>
> = ({ value, onChange, context }) => {
const labelWidth = 10;
const views = useMemo(() => {
@@ -3,7 +3,7 @@ import React, { FC } from 'react';
import { useObservable } from 'react-use';
import { Observable, of } from 'rxjs';
import { StandardEditorProps } from '@grafana/data';
import { FieldConfigPropertyItem, StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data';
import {
ColorPicker,
Field,
@@ -86,12 +86,12 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
onChange({ ...value, textConfig: { ...value.textConfig, offsetY } });
};
const onTextAlignChange = (textAlign: unknown) => {
onChange({ ...value, textConfig: { ...value.textConfig, textAlign: textAlign as TextAlignment } });
const onTextAlignChange = (textAlign: TextAlignment) => {
onChange({ ...value, textConfig: { ...value.textConfig, textAlign: textAlign } });
};
const onTextBaselineChange = (textBaseline: unknown) => {
onChange({ ...value, textConfig: { ...value.textConfig, textBaseline: textBaseline as TextBaseline } });
const onTextBaselineChange = (textBaseline: TextBaseline) => {
onChange({ ...value, textConfig: { ...value.textConfig, textBaseline: textBaseline } });
};
const propertyOptions = useObservable(settings?.layerInfo ?? of());
@@ -119,7 +119,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
placeholderValue: defaultStyleConfig.symbol.fixed,
showSourceRadio: false,
},
} as any
} as StandardEditorsRegistryItem
}
/>
</InlineField>
@@ -135,7 +135,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
min: defaultStyleConfig.rotation.min,
max: defaultStyleConfig.rotation.max,
},
} as any
} as StandardEditorsRegistryItem
}
/>
</Field>
@@ -166,7 +166,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
max: 1,
step: 0.1,
},
} as any
} as FieldConfigPropertyItem
}
/>
</InlineField>
@@ -188,7 +188,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
min: 1,
max: 100,
},
} as any
} as StandardEditorsRegistryItem
}
/>
</Field>
@@ -206,7 +206,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
placeholderValue: defaultStyleConfig.symbol.fixed,
showSourceRadio: false,
},
} as any
} as StandardEditorsRegistryItem
}
/>
</Field>
@@ -215,7 +215,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
value={value?.color ?? defaultStyleConfig.color}
context={context}
onChange={onColorChange}
item={{} as any}
item={{} as StandardEditorsRegistryItem}
/>
</Field>
<Field label={'Fill opacity'}>
@@ -230,7 +230,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
max: 1,
step: 0.1,
},
} as any
} as FieldConfigPropertyItem
}
/>
</Field>
@@ -246,7 +246,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
min: defaultStyleConfig.rotation.min,
max: defaultStyleConfig.rotation.max,
},
} as any
} as StandardEditorsRegistryItem
}
/>
</Field>
@@ -256,7 +256,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
value={value?.text ?? defaultTextConfig}
context={context}
onChange={onTextChange}
item={{} as any}
item={{} as StandardEditorsRegistryItem}
/>
</Field>
@@ -268,7 +268,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
value={value?.textConfig?.fontSize ?? defaultStyleConfig.textConfig.fontSize}
context={context}
onChange={onTextFontSizeChange}
item={{} as any}
item={{} as FieldConfigPropertyItem}
/>
</Field>
<Field label={'X offset'}>
@@ -276,7 +276,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
value={value?.textConfig?.offsetX ?? defaultStyleConfig.textConfig.offsetX}
context={context}
onChange={onTextOffsetXChange}
item={{} as any}
item={{} as FieldConfigPropertyItem}
/>
</Field>
<Field label={'Y offset'}>
@@ -284,7 +284,7 @@ export const StyleEditor: FC<StandardEditorProps<StyleConfig, StyleEditorOptions
value={value?.textConfig?.offsetY ?? defaultStyleConfig.textConfig.offsetY}
context={context}
onChange={onTextOffsetYChange}
item={{} as any}
item={{} as FieldConfigPropertyItem}
/>
</Field>
</HorizontalGroup>
@@ -4,7 +4,7 @@ import React, { FC, useCallback, useMemo } from 'react';
import { useObservable } from 'react-use';
import { Observable } from 'rxjs';
import { GrafanaTheme2, SelectableValue, StandardEditorProps } from '@grafana/data';
import { GrafanaTheme2, SelectableValue, StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data';
import { Button, InlineField, InlineFieldRow, Select, useStyles2 } from '@grafana/ui';
import { NumberInput } from 'app/core/components/OptionsUI/NumberInput';
@@ -30,7 +30,7 @@ const comparators = [
{ label: '<=', value: ComparisonOperation.LTE },
];
export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, any, StyleRuleEditorSettings>> = (
export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, unknown, StyleRuleEditorSettings>> = (
props
) => {
const { value, onChange, item, context } = props;
@@ -199,7 +199,7 @@ export const StyleRuleEditor: FC<StandardEditorProps<FeatureStyleConfig, any, an
simpleFixedValues: true,
layerInfo,
},
} as any
} as StandardEditorsRegistryItem
}
/>
</div>
@@ -27,13 +27,12 @@ export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<Map
return { ...parent, options: opts.state.options, instanceState: opts.state };
},
getValue: (path: string) => lodashGet(opts.state.options, path),
onChange: (path: string, value: any) => {
onChange: (path: string, value: string) => {
const { state } = opts;
const { options } = state;
if (path === 'type' && value) {
const layer = geomapLayerRegistry.getIfExists(value);
if (layer) {
console.log('Change layer type:', value, state);
const opts = {
...options, // keep current shared options
type: layer.id,
@@ -55,7 +54,6 @@ export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<Map
}),
build: (builder, context) => {
if (!opts.state) {
console.log('MISSING LAYER!!!', opts);
return;
}
@@ -56,10 +56,10 @@ export function getGlobalStyles(theme: GrafanaTheme2) {
color: ${theme.colors.text.primary}; // #eee;
}
.ol-control {
background-color: ${theme.colors.background.secondary}; //rgba(255,255,255,0.4);
background-color: ${theme.colors.background.primary}; //rgba(255,255,255,0.4);
}
.ol-control:hover {
background-color: ${theme.colors.action.hover}; // rgba(255,255,255,0.6);
background-color: ${theme.colors.background.secondary}; // rgba(255,255,255,0.6);
}
.ol-control button {
color: ${theme.colors.secondary.text}; // white;
+17 -7
View File
@@ -4,12 +4,12 @@ import { PanelPlugin } from '@grafana/data';
import { config } from '@grafana/runtime';
import { commonOptionsBuilder } from '@grafana/ui';
import { GeomapInstanceState, GeomapPanel } from './GeomapPanel';
import { GeomapPanel } from './GeomapPanel';
import { LayersEditor } from './editor/LayersEditor';
import { MapViewEditor } from './editor/MapViewEditor';
import { getLayerEditor } from './editor/layerEditor';
import { mapPanelChangedHandler, mapMigrationHandler } from './migrations';
import { defaultView, GeomapPanelOptions, TooltipMode } from './types';
import { defaultView, GeomapPanelOptions, TooltipMode, GeomapInstanceState } from './types';
export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
.setNoPadding()
@@ -40,12 +40,15 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
defaultValue: defaultView.shared,
});
// eslint-disable-next-line
const state = context.instanceState as GeomapInstanceState;
if (!state?.layers) {
// TODO? show spinner?
} else {
const layersCategory = ['Map layers'];
const basemapCategory = ['Basemap layer'];
builder.addCustomEditor({
category: ['Data layer'],
category: layersCategory,
id: 'layers',
path: '',
name: '',
@@ -57,7 +60,7 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
builder.addNestedOptions(
getLayerEditor({
state: selected,
category: ['Data layer'],
category: layersCategory,
basemaps: false,
})
);
@@ -66,18 +69,18 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
const baselayer = state.layers[0];
if (config.geomapDisableCustomBaseLayer) {
builder.addCustomEditor({
category: ['Base layer'],
category: basemapCategory,
id: 'layers',
path: '',
name: '',
// eslint-disable-next-line react/display-name
editor: () => <div>The base layer is configured by the server admin.</div>,
editor: () => <div>The basemap layer is configured by the server admin.</div>,
});
} else if (baselayer) {
builder.addNestedOptions(
getLayerEditor({
state: baselayer,
category: ['Base layer'],
category: basemapCategory,
basemaps: true,
})
);
@@ -115,6 +118,13 @@ export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
description: 'Indicate map scale',
defaultValue: false,
})
.addBooleanSwitch({
category,
path: 'controls.showMeasure',
name: 'Show measure tools',
description: 'Show tools for making measurements on the map',
defaultValue: false,
})
.addBooleanSwitch({
category,
path: 'controls.showDebug',
@@ -2,7 +2,6 @@
"type": "panel",
"name": "Geomap",
"id": "geomap",
"state": "beta",
"info": {
"description": "Geomap panel",
@@ -267,7 +267,7 @@ async function prepareSVG(url: string, size?: number): Promise<string> {
return `data:image/svg+xml,${svgURI}`;
})
.catch((error) => {
console.error(error);
console.error(error); // eslint-disable-line no-console
return '';
});
}
+22 -1
View File
@@ -1,3 +1,4 @@
import { Map as OpenLayersMap } from 'ol';
import { FeatureLike } from 'ol/Feature';
import BaseLayer from 'ol/layer/Base';
import Units from 'ol/proj/Units';
@@ -26,6 +27,9 @@ export interface ControlsOptions {
// Show debug
showDebug?: boolean;
// Show measure
showMeasure?: boolean;
}
export enum TooltipMode {
@@ -66,16 +70,33 @@ export interface GeomapPanelOptions {
layers: MapLayerOptions[];
tooltip: TooltipOptions;
}
export interface FeatureStyleConfig {
style?: StyleConfig;
check?: FeatureRuleConfig;
}
export interface FeatureRuleConfig {
property: string;
operation: ComparisonOperation;
value: string | boolean | number;
}
export interface GeomapLayerActions {
selectLayer: (uid: string) => void;
deleteLayer: (uid: string) => void;
addlayer: (type: string) => void;
reorder: (src: number, dst: number) => void;
canRename: (v: string) => boolean;
}
export interface GeomapInstanceState {
map?: OpenLayersMap;
layers: MapLayerState[];
selected: number;
actions: GeomapLayerActions;
}
export enum ComparisonOperation {
EQ = 'eq',
NEQ = 'neq',
@@ -88,7 +109,7 @@ export enum ComparisonOperation {
//-------------------
// Runtime model
//-------------------
export interface MapLayerState<TConfig = any> extends LayerElement {
export interface MapLayerState<TConfig = unknown> extends LayerElement {
options: MapLayerOptions<TConfig>;
handler: MapLayerHandler;
layer: BaseLayer; // the openlayers instance
@@ -0,0 +1,80 @@
import { cloneDeep } from 'lodash';
import { FrameGeometrySourceMode } from '@grafana/data/src';
import { GeomapPanel } from '../GeomapPanel';
import { geomapLayerRegistry } from '../layers/registry';
import { GeomapLayerActions, MapLayerState } from '../types';
import { initLayer } from './layers';
import { getNextLayerName } from './utils';
export const getActions = (panel: GeomapPanel) => {
const actions: GeomapLayerActions = {
selectLayer: (uid: string) => {
const selected = panel.layers.findIndex((v) => v.options.name === uid);
if (panel.panelContext && panel.panelContext.onInstanceStateChange) {
panel.panelContext.onInstanceStateChange({
map: panel.map,
layers: panel.layers,
selected,
actions: panel.actions,
});
}
},
canRename: (v: string) => {
return !panel.byName.has(v);
},
deleteLayer: (uid: string) => {
const layers: MapLayerState[] = [];
for (const lyr of panel.layers) {
if (lyr.options.name === uid) {
panel.map?.removeLayer(lyr.layer);
} else {
layers.push(lyr);
}
}
panel.layers = layers;
panel.doOptionsUpdate(0);
},
addlayer: (type: string) => {
const item = geomapLayerRegistry.getIfExists(type);
if (!item) {
return; // ignore empty request
}
initLayer(
panel,
panel.map!,
{
type: item.id,
name: getNextLayerName(panel),
config: cloneDeep(item.defaultOptions),
location: item.showLocation ? { mode: FrameGeometrySourceMode.Auto } : undefined,
tooltip: true,
},
false
).then((lyr) => {
panel.layers = panel.layers.slice(0);
panel.layers.push(lyr);
panel.map?.addLayer(lyr.layer);
panel.doOptionsUpdate(panel.layers.length - 1);
});
},
reorder: (startIndex: number, endIndex: number) => {
const result = Array.from(panel.layers);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
panel.layers = result;
panel.doOptionsUpdate(endIndex);
// Add the layers in the right order
const group = panel.map?.getLayers()!;
group.clear();
panel.layers.forEach((v) => group.push(v.layer));
},
};
return actions;
};
@@ -0,0 +1,155 @@
import { PluggableMap } from 'ol';
import { FeatureLike } from 'ol/Feature';
import { Subject } from 'rxjs';
import { getFrameMatchers, MapLayerHandler, MapLayerOptions, PanelData } from '@grafana/data/src';
import { config } from '@grafana/runtime/src';
import { GeomapPanel } from '../GeomapPanel';
import { MARKERS_LAYER_ID } from '../layers/data/markersLayer';
import { DEFAULT_BASEMAP_CONFIG, geomapLayerRegistry } from '../layers/registry';
import { MapLayerState } from '../types';
import { getNextLayerName } from './utils';
export const applyLayerFilter = (
handler: MapLayerHandler<unknown>,
options: MapLayerOptions<unknown>,
panelDataProps: PanelData
): void => {
if (handler.update) {
let panelData = panelDataProps;
if (options.filterData) {
const matcherFunc = getFrameMatchers(options.filterData);
panelData = {
...panelData,
series: panelData.series.filter(matcherFunc),
};
}
handler.update(panelData);
}
};
export async function updateLayer(panel: GeomapPanel, uid: string, newOptions: MapLayerOptions): Promise<boolean> {
if (!panel.map) {
return false;
}
const current = panel.byName.get(uid);
if (!current) {
return false;
}
let layerIndex = -1;
const group = panel.map?.getLayers()!;
for (let i = 0; i < group?.getLength(); i++) {
if (group.item(i) === current.layer) {
layerIndex = i;
break;
}
}
// Special handling for rename
if (newOptions.name !== uid) {
if (!newOptions.name) {
newOptions.name = uid;
} else if (panel.byName.has(newOptions.name)) {
return false;
}
panel.byName.delete(uid);
uid = newOptions.name;
panel.byName.set(uid, current);
}
// Type changed -- requires full re-initalization
if (current.options.type !== newOptions.type) {
// full init
} else {
// just update options
}
const layers = panel.layers.slice(0);
try {
const info = await initLayer(panel, panel.map, newOptions, current.isBasemap);
layers[layerIndex]?.handler.dispose?.();
layers[layerIndex] = info;
group.setAt(layerIndex, info.layer);
// initialize with new data
applyLayerFilter(info.handler, newOptions, panel.props.data);
} catch (err) {
console.warn('ERROR', err); // eslint-disable-line no-console
return false;
}
// Just to trigger a state update
panel.setState({ legends: [] });
panel.layers = layers;
panel.doOptionsUpdate(layerIndex);
return true;
}
export async function initLayer(
panel: GeomapPanel,
map: PluggableMap,
options: MapLayerOptions,
isBasemap?: boolean
): Promise<MapLayerState> {
if (isBasemap && (!options?.type || config.geomapDisableCustomBaseLayer)) {
options = DEFAULT_BASEMAP_CONFIG;
}
// Use default makers layer
if (!options?.type) {
options = {
type: MARKERS_LAYER_ID,
name: getNextLayerName(panel),
config: {},
};
}
const item = geomapLayerRegistry.getIfExists(options.type);
if (!item) {
return Promise.reject('unknown layer: ' + options.type);
}
const handler = await item.create(map, options, panel.props.eventBus, config.theme2);
const layer = handler.init(); // eslint-disable-line
if (options.opacity != null) {
layer.setOpacity(options.opacity);
}
if (!options.name) {
options.name = getNextLayerName(panel);
}
const UID = options.name;
const state: MapLayerState<unknown> = {
// UID, // unique name when added to the map (it may change and will need special handling)
isBasemap,
options,
layer,
handler,
mouseEvents: new Subject<FeatureLike | undefined>(),
getName: () => UID,
// Used by the editors
onChange: (cfg: MapLayerOptions) => {
updateLayer(panel, UID, cfg);
},
};
panel.byName.set(UID, state);
// eslint-disable-next-line
(state.layer as any).__state = state;
applyLayerFilter(handler, options, panel.props.data);
return state;
}
export const getMapLayerState = (l: any) => {
return l?.__state as MapLayerState;
};
@@ -0,0 +1,16 @@
import { measures } from './measure';
describe('get measure utils', () => {
it('switch from length to area', () => {
const length = measures[0];
expect(length.value).toBe('length');
expect(length.getUnit('ft').value).toBe('ft');
expect(length.getUnit('ft2').value).toBe('ft');
});
it('switch from area to length', () => {
const area = measures[1];
expect(area.value).toBe('area');
expect(area.getUnit('ft2').value).toBe('ft2');
expect(area.getUnit('ft').value).toBe('ft2');
});
});
@@ -0,0 +1,106 @@
import { FormattedValue, getValueFormat, SelectableValue, toFixedUnit } from '@grafana/data';
type MeasureAction = 'area' | 'length';
export interface MapMeasureOptions {
action: MeasureAction;
unit: string;
color?: string;
}
export interface MapMeasure extends SelectableValue<MeasureAction> {
geometry: 'Polygon' | 'LineString';
units: MapUnit[];
/**
* This will get the best unit for the selected string or a default
* This is helpful when converting from area<>length, we should try to stay
* in the same category of unit if possible
*/
getUnit: (v?: string) => MapUnit;
}
export interface MapUnit extends SelectableValue<string> {
format: (si: number) => FormattedValue;
}
export const measures: MapMeasure[] = [
{
value: 'length',
label: 'Length',
geometry: 'LineString',
units: [
{
label: 'Metric (m/km)',
value: 'm',
format: (m: number) => getValueFormat('lengthm')(m),
},
{
label: 'Feet (ft)',
value: 'ft',
format: (m: number) => getValueFormat('lengthft')(m * 3.28084),
},
{
label: 'Miles (mi)',
value: 'mi',
format: (m: number) => getValueFormat('lengthmi')(m / 1609.0),
},
{
label: 'Nautical miles (nmi)',
value: 'nmi',
format: (m: number) => getValueFormat('nmi')(m / 1852.0),
},
],
getUnit: (v?: string) => {
const units = measures[0].units;
if (v?.endsWith('2')) {
v = v.substring(0, v.length - 1);
}
return units.find((u) => u.value === v) ?? units[0];
},
},
{
value: 'area',
label: 'Area',
geometry: 'Polygon',
units: [
{
label: 'Square Meters (m²)',
value: 'm2',
format: (m2: number) => getValueFormat('areaM2')(m2),
},
{
label: 'Square Kilometers (km²)',
value: 'km2',
format: (m2: number) => toFixedUnit('km²')(m2 * 1e-6),
},
{
label: 'Square Feet (ft²)',
value: 'ft2',
format: (m2: number) => getValueFormat('areaF2')(m2 * 10.76391),
},
{
label: 'Square Miles (mi²)',
value: 'mi2',
format: (m2: number) => getValueFormat('areaMI2')(m2 * 3.861e-7),
},
{
label: 'Acres',
value: 'acre2',
format: (m2: number) => toFixedUnit('acre')(m2 * 2.47105e-4),
},
{
label: 'Hectare',
value: 'hectare2',
format: (m2: number) => toFixedUnit('ha')(m2 * 1e-4),
},
],
getUnit: (v?: string) => {
const units = measures[1].units;
if (!v?.endsWith('2')) {
v += '2';
}
return units.find((u) => u.value === v) ?? units[0];
},
},
];
@@ -0,0 +1,111 @@
import { MapBrowserEvent } from 'ol';
import { toLonLat } from 'ol/proj';
import { DataFrame, DataHoverClearEvent } from '@grafana/data/src';
import { GeomapPanel } from '../GeomapPanel';
import { GeomapHoverPayload, GeomapLayerHover } from '../event';
import { MapLayerState } from '../types';
import { getMapLayerState } from './layers';
export const setTooltipListeners = (panel: GeomapPanel) => {
// Tooltip listener
panel.map?.on('singleclick', panel.pointerClickListener);
panel.map?.on('pointermove', panel.pointerMoveListener);
panel.map?.getViewport().addEventListener('mouseout', (evt: MouseEvent) => {
panel.props.eventBus.publish(new DataHoverClearEvent());
});
};
export const pointerClickListener = (evt: MapBrowserEvent<UIEvent>, panel: GeomapPanel) => {
if (pointerMoveListener(evt, panel)) {
evt.preventDefault();
evt.stopPropagation();
panel.mapDiv!.style.cursor = 'auto';
panel.setState({ ttipOpen: true });
}
};
export const pointerMoveListener = (evt: MapBrowserEvent<UIEvent>, panel: GeomapPanel) => {
// If measure menu is open, bypass tooltip logic and display measuring mouse events
if (panel.state.measureMenuActive) {
return true;
}
if (!panel.map || panel.state.ttipOpen) {
return false;
}
const mouse = evt.originalEvent as MouseEvent; // eslint-disable-line
const pixel = panel.map.getEventPixel(mouse);
const hover = toLonLat(panel.map.getCoordinateFromPixel(pixel));
const { hoverPayload } = panel;
hoverPayload.pageX = mouse.pageX;
hoverPayload.pageY = mouse.pageY;
hoverPayload.point = {
lat: hover[1],
lon: hover[0],
};
hoverPayload.data = undefined;
hoverPayload.columnIndex = undefined;
hoverPayload.rowIndex = undefined;
hoverPayload.layers = undefined;
const layers: GeomapLayerHover[] = [];
const layerLookup = new Map<MapLayerState, GeomapLayerHover>();
let ttip: GeomapHoverPayload = {} as GeomapHoverPayload;
panel.map.forEachFeatureAtPixel(
pixel,
(feature, layer, geo) => {
const s: MapLayerState = getMapLayerState(layer);
//match hover layer to layer in layers
//check if the layer show tooltip is enabled
//then also pass the list of tooltip fields if exists
//this is used as the generic hover event
if (!hoverPayload.data) {
const props = feature.getProperties();
const frame = props['frame'];
if (frame) {
hoverPayload.data = ttip.data = frame as DataFrame;
hoverPayload.rowIndex = ttip.rowIndex = props['rowIndex'];
}
if (s?.mouseEvents) {
s.mouseEvents.next(feature);
}
}
if (s) {
let h = layerLookup.get(s);
if (!h) {
h = { layer: s, features: [] };
layerLookup.set(s, h);
layers.push(h);
}
h.features.push(feature);
}
},
{
layerFilter: (l) => {
const hoverLayerState = getMapLayerState(l);
return hoverLayerState?.options?.tooltip !== false;
},
}
);
panel.hoverPayload.layers = layers.length ? layers : undefined;
panel.props.eventBus.publish(panel.hoverEvent);
panel.setState({ ttip: { ...hoverPayload } });
if (!layers.length) {
// clear mouse events
panel.layers.forEach((layer) => {
layer.mouseEvents.next(undefined);
});
}
const found = Boolean(layers.length);
panel.mapDiv!.style.cursor = found ? 'pointer' : 'auto';
return found;
};
@@ -1,9 +1,16 @@
import { Map as OpenLayersMap } from 'ol';
import { defaults as interactionDefaults } from 'ol/interaction';
import { SelectableValue } from '@grafana/data';
import { DataFrame, GrafanaTheme2 } from '@grafana/data/src';
import { getColorDimension, getScalarDimension, getScaledDimension, getTextDimension } from 'app/features/dimensions';
import { getGrafanaDatasource } from 'app/plugins/datasource/grafana/datasource';
import { GeomapPanel } from '../GeomapPanel';
import { defaultStyleConfig, StyleConfig, StyleConfigState, StyleDimensions } from '../style/types';
import { GeomapPanelOptions, MapLayerState } from '../types';
import { initMapView } from './view';
export function getStyleDimension(
frame: DataFrame | undefined,
@@ -68,3 +75,46 @@ async function initGeojsonFiles() {
});
}
}
export const getNewOpenLayersMap = (panel: GeomapPanel, options: GeomapPanelOptions, div: HTMLDivElement) => {
const [view] = initMapView(options.view, undefined, undefined);
return (panel.map = new OpenLayersMap({
view: view,
pixelRatio: 1, // or zoom?
layers: [], // loaded explicitly below
controls: [],
target: div,
interactions: interactionDefaults({
mouseWheelZoom: false, // managed by initControls
}),
}));
};
export const updateMap = (panel: GeomapPanel, options: GeomapPanelOptions) => {
panel.initControls(options.controls);
panel.forceUpdate(); // first render
};
export const notifyPanelEditor = (geomapPanel: GeomapPanel, layers: MapLayerState[], selected: number) => {
// Notify the panel editor
if (geomapPanel.panelContext && geomapPanel.panelContext.onInstanceStateChange) {
geomapPanel.panelContext.onInstanceStateChange({
map: geomapPanel.map,
layers: layers,
selected: selected,
actions: geomapPanel.actions,
});
}
};
export const getNextLayerName = (panel: GeomapPanel) => {
let idx = panel.layers.length; // since basemap is 0, this looks right
while (true && idx < 100) {
const name = `Layer ${idx++}`;
if (!panel.byName.has(name)) {
return name;
}
}
return `Layer ${Date.now()}`;
};
@@ -0,0 +1,73 @@
import { View, Collection } from 'ol';
import { Coordinate } from 'ol/coordinate';
import { isEmpty } from 'ol/extent';
import BaseLayer from 'ol/layer/Base';
import { fromLonLat } from 'ol/proj';
import { MapViewConfig } from '../types';
import { centerPointRegistry, MapCenterID } from '../view';
import { getLayersExtent } from './getLayersExtent';
export const initViewExtent = (view: View, config: MapViewConfig, layers: Collection<BaseLayer>) => {
const v = centerPointRegistry.getIfExists(config.id);
if (v) {
let coord: Coordinate | undefined = undefined;
if (v.lat == null) {
if (v.id === MapCenterID.Coordinates) {
coord = [config.lon ?? 0, config.lat ?? 0];
} else if (v.id === MapCenterID.Fit) {
const extent = getLayersExtent(layers);
if (!isEmpty(extent)) {
view.fit(extent, {
padding: [30, 30, 30, 30],
maxZoom: config.zoom ?? config.maxZoom,
});
}
} else {
// TODO: view requires special handling
}
} else {
coord = [v.lon ?? 0, v.lat ?? 0];
}
if (coord) {
view.setCenter(fromLonLat(coord));
}
}
if (config.maxZoom) {
view.setMaxZoom(config.maxZoom);
}
if (config.minZoom) {
view.setMaxZoom(config.minZoom);
}
if (config.zoom && v?.id !== MapCenterID.Fit) {
view.setZoom(config.zoom);
}
};
export const initMapView = (
config: MapViewConfig,
sharedView?: View | undefined,
layers?: Collection<BaseLayer>
): Array<View | undefined> => {
let view = new View({
center: [0, 0],
zoom: 1,
showFullExtent: true, // allows zooming so the full range is visible
});
// With shared views, all panels use the same view instance
if (config.shared) {
if (!sharedView) {
sharedView = view;
} else {
view = sharedView;
}
}
if (layers) {
initViewExtent(view, config, layers);
}
return [sharedView, view];
};