From fe0f19318989bd3616f68ce7546e68a4c2d6cc5f Mon Sep 17 00:00:00 2001 From: Drew Slobodnjak <60050885+drew08t@users.noreply.github.com> Date: Wed, 3 Aug 2022 16:19:30 -0700 Subject: [PATCH] Geomap: Add measuring tools (#51608) * Geomap: add measuring tools * Add measure type selection * Add controls and state to measure overlay * Override tooltip mouse events when menu active * Move measure tools to top right * Lay groundwork for units and consolidate measuring * Create measure vector layer class * Improve styling to match other overlay controls * Consolidate styling and use theme2 * Update unit language and add km2 Co-authored-by: Ryan McKinley Co-authored-by: nmarrs --- .betterer.results | 5 + packages/grafana-ui/src/types/icon.ts | 1 + .../plugins/panel/geomap/GeomapOverlay.tsx | 60 ++--- .../app/plugins/panel/geomap/GeomapPanel.tsx | 41 ++- .../geomap/components/MeasureOverlay.tsx | 139 ++++++++++ .../geomap/components/MeasureVectorLayer.ts | 255 ++++++++++++++++++ .../app/plugins/panel/geomap/globalStyles.ts | 4 +- public/app/plugins/panel/geomap/module.tsx | 7 + public/app/plugins/panel/geomap/types.ts | 3 + .../panel/geomap/utils/measure.test.ts | 16 ++ .../app/plugins/panel/geomap/utils/measure.ts | 106 ++++++++ 11 files changed, 595 insertions(+), 42 deletions(-) create mode 100644 public/app/plugins/panel/geomap/components/MeasureOverlay.tsx create mode 100644 public/app/plugins/panel/geomap/components/MeasureVectorLayer.ts create mode 100644 public/app/plugins/panel/geomap/utils/measure.test.ts create mode 100644 public/app/plugins/panel/geomap/utils/measure.ts diff --git a/.betterer.results b/.betterer.results index 836a841afca..5d90f9e39cb 100644 --- a/.betterer.results +++ b/.betterer.results @@ -8639,6 +8639,11 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "5"], [0, 0, 0, "Do not use any type assertions.", "6"] ], + "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"], [0, 0, 0, "Do not use any type assertions.", "1"] diff --git a/packages/grafana-ui/src/types/icon.ts b/packages/grafana-ui/src/types/icon.ts index 89f03efab0c..739eb7e04ca 100644 --- a/packages/grafana-ui/src/types/icon.ts +++ b/packages/grafana-ui/src/types/icon.ts @@ -148,6 +148,7 @@ export const getAvailableIcons = () => 'record-audio', 'repeat', 'rocket', + 'ruler-combined', 'save', 'search', 'search-minus', diff --git a/public/app/plugins/panel/geomap/GeomapOverlay.tsx b/public/app/plugins/panel/geomap/GeomapOverlay.tsx index 6128054f2c8..0c48318a4a8 100644 --- a/public/app/plugins/panel/geomap/GeomapOverlay.tsx +++ b/public/app/plugins/panel/geomap/GeomapOverlay.tsx @@ -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 { - 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 ( +
+ {Boolean(topRight1?.length) &&
{topRight1}
} + {Boolean(topRight2?.length) &&
{topRight2}
} + {Boolean(bottomLeft?.length) && ( +
+ {bottomLeft} +
+ )} +
+ ); +}; - constructor(props: OverlayProps) { - super(props); - } - - render() { - const { topRight, bottomLeft } = this.props; - return ( -
- {Boolean(topRight?.length) &&
{topRight}
} - {Boolean(bottomLeft?.length) && ( -
- {bottomLeft} -
- )} -
- ); - } -} - -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; `, -})); +}); diff --git a/public/app/plugins/panel/geomap/GeomapPanel.tsx b/public/app/plugins/panel/geomap/GeomapPanel.tsx index 58326a30bd5..41c6cb070e7 100644 --- a/public/app/plugins/panel/geomap/GeomapPanel.tsx +++ b/public/app/plugins/panel/geomap/GeomapPanel.tsx @@ -34,6 +34,7 @@ import { PanelEditExitedEvent } from 'app/types/events'; import { GeomapOverlay, OverlayProps } from './GeomapOverlay'; import { GeomapTooltip } from './GeomapTooltip'; import { DebugOverlay } from './components/DebugOverlay'; +import { MeasureOverlay } from './components/MeasureOverlay'; import { GeomapHoverPayload, GeomapLayerHover } from './event'; import { getGlobalStyles } from './globalStyles'; import { defaultMarkersConfig, MARKERS_LAYER_ID } from './layers/data/markersLayer'; @@ -50,6 +51,7 @@ interface State extends OverlayProps { ttip?: GeomapHoverPayload; ttipOpen: boolean; legends: ReactNode[]; + measureMenuActive?: boolean; } export interface GeomapLayerActions { @@ -246,15 +248,11 @@ export class GeomapPanel extends Component { */ 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())); } if (options.controls !== oldOptions.controls) { - console.log('Controls changed'); this.initControls(options.controls ?? { showZoom: true, showAttribution: true }); } } @@ -361,6 +359,10 @@ export class GeomapPanel extends Component { }; pointerMoveListener = (evt: MapBrowserEvent) => { + // If measure menu is open, bypass tooltip logic and display measuring mouse events + if (this.state.measureMenuActive) { + return true; + } if (!this.map || this.state.ttipOpen) { return false; } @@ -652,12 +654,26 @@ export class GeomapPanel extends Component { } // Update the react overlays - let topRight: ReactNode[] = []; - if (options.showDebug) { - topRight = []; + let topRight1: ReactNode[] = []; + if (options.showMeasure) { + topRight1 = [ + { + this.setState({ ttipOpen: value, measureMenuActive: value }); + }} + />, + ]; } - this.setState({ topRight }); + let topRight2: ReactNode[] = []; + if (options.showDebug) { + topRight2 = []; + } + + this.setState({ topRight1, topRight2 }); } getLegends() { @@ -672,7 +688,7 @@ export class GeomapPanel extends Component { } 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 +700,12 @@ export class GeomapPanel extends Component {
- +
diff --git a/public/app/plugins/panel/geomap/components/MeasureOverlay.tsx b/public/app/plugins/panel/geomap/components/MeasureOverlay.tsx new file mode 100644 index 00000000000..f11802ab2b2 --- /dev/null +++ b/public/app/plugins/panel/geomap/components/MeasureOverlay.tsx @@ -0,0 +1,139 @@ +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(true); + const [menuActive, setMenuActive] = useState(false); + + // Options State + const [options, setOptions] = useState({ + 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); + 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 ( +
+ {menuActive ? ( +
+
+ { + 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); + }} + /> +
+