diff --git a/devenv/dev-dashboards/panel-geomap/geomap-route-layer.json b/devenv/dev-dashboards/panel-geomap/geomap-route-layer.json new file mode 100644 index 00000000000..e1e848d6660 --- /dev/null +++ b/devenv/dev-dashboards/panel-geomap/geomap-route-layer.json @@ -0,0 +1,179 @@ +{ + "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, + "id": 231, + "links": [], + "liveNow": false, + "panels": [ + { + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-RdYlGr" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "dark-red", + "value": 0 + }, + { + "color": "yellow", + "value": 50 + }, + { + "color": "green", + "value": 100 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 16, + "w": 18, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "basemap": { + "config": { + "server": "streets" + }, + "name": "Layer 0", + "type": "esri-xyz" + }, + "controls": { + "mouseWheelZoom": true, + "showAttribution": true, + "showDebug": false, + "showMeasure": false, + "showScale": false, + "showZoom": true + }, + "layers": [ + { + "config": { + "arrow": 1, + "style": { + "color": { + "field": "val", + "fixed": "dark-green" + }, + "lineWidth": 2, + "opacity": 1, + "rotation": { + "fixed": 0, + "max": 360, + "min": -360, + "mode": "mod" + }, + "size": { + "field": "val", + "fixed": 5, + "max": 20, + "min": 5 + }, + "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 2", + "tooltip": true, + "type": "route" + } + ], + "tooltip": { + "mode": "details" + }, + "view": { + "allLayers": true, + "id": "coords", + "lat": 2.359794, + "lon": 8.135816, + "zoom": 4.45 + } + }, + "pluginVersion": "9.4.0-pre", + "targets": [ + { + "csvContent": "lat,lon,val\n-5,2,0\n1,5,25\n6,10,50\n9,15,75\n10,20,100", + "datasource": { + "type": "testdata", + "uid": "PD8C576611E62080A" + }, + "refId": "A", + "scenarioId": "csv_content" + } + ], + "title": "Route with Colors", + "type": "geomap" + } + ], + "schemaVersion": 37, + "style": "dark", + "tags": ["gdev", "panel-tests", "geomap"], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Panel Tests - Geomap Route Layer", + "uid": "OYTKK3DVk", + "version": 25, + "weekStart": "" +} diff --git a/devenv/jsonnet/dev-dashboards.libsonnet b/devenv/jsonnet/dev-dashboards.libsonnet index f5787a61e6f..db37c3de6e6 100644 --- a/devenv/jsonnet/dev-dashboards.libsonnet +++ b/devenv/jsonnet/dev-dashboards.libsonnet @@ -205,6 +205,13 @@ local dashboard = grafana.dashboard; id: 0, } }, + dashboard.new('geomap-route-layer', import '../dev-dashboards/panel-geomap/geomap-route-layer.json') + + resource.addMetadata('folder', 'dev-dashboards') + + { + spec+: { + id: 0, + } + }, dashboard.new('geomap-spatial-operations-transformer', import '../dev-dashboards/panel-geomap/geomap-spatial-operations-transformer.json') + resource.addMetadata('folder', 'dev-dashboards') + { diff --git a/public/app/plugins/panel/geomap/layers/data/routeLayer.tsx b/public/app/plugins/panel/geomap/layers/data/routeLayer.tsx index 7bd573c1763..6ae014f2043 100644 --- a/public/app/plugins/panel/geomap/layers/data/routeLayer.tsx +++ b/public/app/plugins/panel/geomap/layers/data/routeLayer.tsx @@ -15,8 +15,7 @@ import Map from 'ol/Map'; import { FeatureLike } from 'ol/Feature'; import { Subscription, throttleTime } from 'rxjs'; import { getGeometryField, getLocationMatchers } from 'app/features/geo/utils/location'; -import { getColorDimension } from 'app/features/dimensions'; -import { defaultStyleConfig, StyleConfig, StyleDimensions } from '../../style/types'; +import { defaultStyleConfig, StyleConfig } from '../../style/types'; import { StyleEditor } from '../../editor/StyleEditor'; import { getStyleConfigState } from '../../style/utils'; import VectorLayer from 'ol/layer/Vector'; @@ -28,10 +27,15 @@ import VectorSource from 'ol/source/Vector'; import { Fill, Stroke, Style, Circle } from 'ol/style'; import Feature from 'ol/Feature'; import { alpha } from '@grafana/data/src/themes/colorManipulator'; +import { LineString, SimpleGeometry } from 'ol/geom'; +import FlowLine from 'ol-ext/style/FlowLine'; +import tinycolor from 'tinycolor2'; +import { getStyleDimension } from '../../utils/utils'; // Configuration options for Circle overlays export interface RouteConfig { style: StyleConfig; + arrow?: 0 | 1 | -1; } const defaultOptions: RouteConfig = { @@ -40,6 +44,7 @@ const defaultOptions: RouteConfig = { opacity: 1, lineWidth: 2, }, + arrow: 0, }; export const ROUTE_LAYER_ID = 'route'; @@ -81,10 +86,16 @@ export const routeLayer: MapLayerRegistryItem = { const location = await getLocationMatchers(options.location); const source = new FrameVectorSource(location); const vectorLayer = new VectorLayer({ source }); + const hasArrows = config.arrow == 1 || config.arrow == -1; - if (!style.fields) { + if (!style.fields && !hasArrows) { // Set a global style - vectorLayer.setStyle(routeStyle(style.base)); + const styleBase = routeStyle(style.base); + if (style.config.size && style.config.size.fixed) { + // Applies width to base style if specified + styleBase.getStroke().setWidth(style.config.size.fixed); + } + vectorLayer.setStyle(styleBase); } else { vectorLayer.setStyle((feature: FeatureLike) => { const idx = feature.get('rowIndex') as number; @@ -93,6 +104,53 @@ export const routeLayer: MapLayerRegistryItem = { return routeStyle(style.base); } + const styles = []; + const geom = feature.getGeometry(); + const opacity = style.config.opacity ?? 1; + if (geom instanceof SimpleGeometry) { + const coordinates = geom.getCoordinates(); + if (coordinates) { + for (let i = 0; i < coordinates.length - 1; i++) { + const color1 = tinycolor( + theme.visualization.getColorByName((dims.color && dims.color.get(i)) ?? style.base.color) + ) + .setAlpha(opacity) + .toString(); + const color2 = tinycolor( + theme.visualization.getColorByName((dims.color && dims.color.get(i + 1)) ?? style.base.color) + ) + .setAlpha(opacity) + .toString(); + + const arrowSize1 = (dims.size && dims.size.get(i)) ?? style.base.size; + const arrowSize2 = (dims.size && dims.size.get(i + 1)) ?? style.base.size; + + const flowStyle = new FlowLine({ + visible: true, + lineCap: config.arrow == 0 ? 'round' : 'square', + color: color1, + color2: color2, + width: (dims.size && dims.size.get(i)) ?? style.base.size, + width2: (dims.size && dims.size.get(i + 1)) ?? style.base.size, + }); + if (config.arrow) { + flowStyle.setArrow(config.arrow); + if (config.arrow > 0) { + flowStyle.setArrowColor(color2); + flowStyle.setArrowSize((arrowSize2 ?? 0) * 1.5); + } else { + flowStyle.setArrowColor(color1); + flowStyle.setArrowSize((arrowSize1 ?? 0) * 1.5); + } + } + const LS = new LineString([coordinates[i], coordinates[i + 1]]); + flowStyle.setGeometry(LS); + styles.push(flowStyle); + } + } + return styles; + } + const values = { ...style.base }; if (dims.color) { @@ -110,10 +168,10 @@ export const routeLayer: MapLayerRegistryItem = { radius: crosshairRadius, stroke: new Stroke({ color: alpha(style.base.color, 0.4), - width: crosshairRadius + 2 + width: crosshairRadius + 2, }), - fill: new Fill({color: style.base.color}), - }) + fill: new Fill({ color: style.base.color }), + }), }); const crosshairLayer = new VectorLayer({ @@ -124,7 +182,7 @@ export const routeLayer: MapLayerRegistryItem = { }); const layer = new LayerGroup({ - layers: [vectorLayer, crosshairLayer] + layers: [vectorLayer, crosshairLayer], }); // Crosshair sharing subscriptions @@ -172,12 +230,8 @@ export const routeLayer: MapLayerRegistryItem = { } for (const frame of data.series) { - if (style.fields) { - const dims: StyleDimensions = {}; - if (style.fields.color) { - dims.color = getColorDimension(frame, style.config.color ?? defaultStyleConfig.color, theme); - } - style.dims = dims; + if (style.fields || hasArrows) { + style.dims = getStyleDimension(frame, style, theme); } source.updateLineString(frame); @@ -194,19 +248,21 @@ export const routeLayer: MapLayerRegistryItem = { name: 'Style', editor: StyleEditor, settings: { - simpleFixedValues: true, + simpleFixedValues: false, }, defaultValue: defaultOptions.style, }) - .addSliderInput({ - path: 'config.style.lineWidth', - name: 'Line width', - defaultValue: defaultOptions.style.lineWidth, + .addRadio({ + path: 'config.arrow', + name: 'Arrow', settings: { - min: 1, - max: 10, - step: 1, + options: [ + { label: 'None', value: 0 }, + { label: 'Forward', value: 1 }, + { label: 'Reverse', value: -1 }, + ], }, + defaultValue: defaultOptions.arrow, }); }, }; @@ -229,7 +285,7 @@ function findNearestTimeIndex(timestamps: number[], time: number): number | null return lastIdx; } - const probableIdx = Math.abs(Math.round(lastIdx * (time - timestamps[0]) / (timestamps[lastIdx] - timestamps[0]))); + const probableIdx = Math.abs(Math.round((lastIdx * (time - timestamps[0])) / (timestamps[lastIdx] - timestamps[0]))); if (time < timestamps[probableIdx]) { for (let i = probableIdx; i > 0; i--) { if (time > timestamps[i]) {