mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Geomap: add initial openlayers alpha panel (#36188)
This commit is contained in:
parent
e4ece0530a
commit
9ce6e2a664
@ -226,6 +226,7 @@
|
||||
"@sentry/browser": "5.25.0",
|
||||
"@sentry/types": "5.24.2",
|
||||
"@sentry/utils": "5.24.2",
|
||||
"@types/ol": "^6.5.1",
|
||||
"@welldone-software/why-did-you-render": "4.0.6",
|
||||
"abortcontroller-polyfill": "1.4.0",
|
||||
"angular": "1.8.2",
|
||||
@ -263,6 +264,7 @@
|
||||
"moment-timezone": "0.5.33",
|
||||
"mousetrap": "1.6.5",
|
||||
"mousetrap-global-bind": "1.1.0",
|
||||
"ol": "^6.5.0",
|
||||
"papaparse": "5.3.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"prismjs": "1.23.0",
|
||||
|
67
packages/grafana-data/src/geo/layer.ts
Normal file
67
packages/grafana-data/src/geo/layer.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { RegistryItemWithOptions } from '../utils/Registry';
|
||||
import BaseLayer from 'ol/layer/Base';
|
||||
import Map from 'ol/Map';
|
||||
import { PanelData } from '../types';
|
||||
import { GrafanaTheme2 } from '../themes';
|
||||
import { PanelOptionsEditorBuilder } from '../utils';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
/**
|
||||
* This gets saved in panel json
|
||||
*
|
||||
* depending on the type, it may have additional config
|
||||
*
|
||||
* This exists in `grafana/data` so the types are well known and extendable but the
|
||||
* layout/frame is control by the map panel
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface MapLayerConfig<TCustom = any> {
|
||||
type: string;
|
||||
name?: string; // configured display name
|
||||
|
||||
// Common properties:
|
||||
// https://openlayers.org/en/latest/apidoc/module-ol_layer_Base-BaseLayer.html
|
||||
// Layer opacity (0-1)
|
||||
opacity?: number;
|
||||
|
||||
// Custom options depending on the type
|
||||
config?: TCustom;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface MapLayerHandler {
|
||||
init: () => BaseLayer;
|
||||
legend?: () => ReactNode;
|
||||
update?: (data: PanelData) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map layer configuration
|
||||
*
|
||||
* @alpha
|
||||
*/
|
||||
export interface MapLayerRegistryItem<TConfig = MapLayerConfig> extends RegistryItemWithOptions {
|
||||
/**
|
||||
* This layer can be used as a background
|
||||
*/
|
||||
isBaseMap?: boolean;
|
||||
|
||||
/**
|
||||
* Show transparency controls in UI (for non-basemaps)
|
||||
*/
|
||||
showTransparency?: boolean;
|
||||
|
||||
/**
|
||||
* Function that configures transformation and returns a transformer
|
||||
* @param options
|
||||
*/
|
||||
create: (map: Map, options: MapLayerConfig<TConfig>, theme: GrafanaTheme2) => MapLayerHandler;
|
||||
|
||||
/**
|
||||
* Show custom elements in the panel edit UI
|
||||
*/
|
||||
registerOptionsUI?: (builder: PanelOptionsEditorBuilder<TConfig>) => void;
|
||||
}
|
@ -15,6 +15,7 @@ export * from './field';
|
||||
export * from './events';
|
||||
export * from './themes';
|
||||
export * from './monaco';
|
||||
export * from './geo/layer';
|
||||
export {
|
||||
ValueMatcherOptions,
|
||||
BasicValueMatcherOptions,
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { StandardEditorContext, VariableSuggestionsScope } from '@grafana/data';
|
||||
import { PanelOptionsEditorItem, StandardEditorContext, VariableSuggestionsScope } from '@grafana/data';
|
||||
import { get as lodashGet } from 'lodash';
|
||||
import { getDataLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
|
||||
import { OptionPaneRenderProps } from './types';
|
||||
@ -7,6 +7,8 @@ import { updateDefaultFieldConfigValue, setOptionImmutably } from './utils';
|
||||
import { OptionsPaneItemDescriptor } from './OptionsPaneItemDescriptor';
|
||||
import { OptionsPaneCategoryDescriptor } from './OptionsPaneCategoryDescriptor';
|
||||
|
||||
type categoryGetter = (categoryNames?: string[]) => OptionsPaneCategoryDescriptor;
|
||||
|
||||
export function getVizualizationOptions(props: OptionPaneRenderProps): OptionsPaneCategoryDescriptor[] {
|
||||
const { plugin, panel, onPanelOptionsChanged, onFieldConfigsChange, data, dashboard } = props;
|
||||
const currentOptions = panel.getOptions();
|
||||
@ -37,39 +39,16 @@ export function getVizualizationOptions(props: OptionPaneRenderProps): OptionsPa
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Panel options
|
||||
*/
|
||||
for (const pluginOption of plugin.optionEditors.list()) {
|
||||
if (pluginOption.showIf && !pluginOption.showIf(currentOptions, data?.series)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const category = getOptionsPaneCategory(pluginOption.category);
|
||||
const Editor = pluginOption.editor;
|
||||
|
||||
category.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: pluginOption.name,
|
||||
description: pluginOption.description,
|
||||
render: function renderEditor() {
|
||||
const onChange = (value: any) => {
|
||||
const newOptions = setOptionImmutably(currentOptions, pluginOption.path, value);
|
||||
onPanelOptionsChanged(newOptions);
|
||||
};
|
||||
|
||||
return (
|
||||
<Editor
|
||||
value={lodashGet(currentOptions, pluginOption.path)}
|
||||
onChange={onChange}
|
||||
item={pluginOption}
|
||||
context={context}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
// Load the options into categories
|
||||
fillOptionsPaneItems(
|
||||
plugin.optionEditors.list(),
|
||||
getOptionsPaneCategory,
|
||||
(path: string, value: any) => {
|
||||
const newOptions = setOptionImmutably(context.options, path, value);
|
||||
onPanelOptionsChanged(newOptions);
|
||||
},
|
||||
context
|
||||
);
|
||||
|
||||
/**
|
||||
* Field options
|
||||
@ -120,3 +99,45 @@ export function getVizualizationOptions(props: OptionPaneRenderProps): OptionsPa
|
||||
|
||||
return Object.values(categoryIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* This will iterate all options panes and add register them with the configured categories
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function fillOptionsPaneItems(
|
||||
optionEditors: PanelOptionsEditorItem[],
|
||||
getOptionsPaneCategory: categoryGetter,
|
||||
onValueChanged: (path: string, value: any) => void,
|
||||
context: StandardEditorContext<any>
|
||||
) {
|
||||
for (const pluginOption of optionEditors) {
|
||||
if (pluginOption.showIf && !pluginOption.showIf(context.options, context.data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const category = getOptionsPaneCategory(pluginOption.category);
|
||||
const Editor = pluginOption.editor;
|
||||
|
||||
// TODO? can some options recursivly call: fillOptionsPaneItems?
|
||||
|
||||
category.addItem(
|
||||
new OptionsPaneItemDescriptor({
|
||||
title: pluginOption.name,
|
||||
description: pluginOption.description,
|
||||
render: function renderEditor() {
|
||||
return (
|
||||
<Editor
|
||||
value={lodashGet(context.options, pluginOption.path)}
|
||||
onChange={(value: any) => {
|
||||
onValueChanged(pluginOption.path, value);
|
||||
}}
|
||||
item={pluginOption}
|
||||
context={context}
|
||||
/>
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -67,6 +67,9 @@ import * as welcomeBanner from 'app/plugins/panel/welcome/module';
|
||||
import * as nodeGraph from 'app/plugins/panel/nodeGraph/module';
|
||||
import * as histogramPanel from 'app/plugins/panel/histogram/module';
|
||||
|
||||
// Async loaded panels
|
||||
const geomapPanel = async () => await import(/* webpackChunkName: "geomapPanel" */ 'app/plugins/panel/geomap/module');
|
||||
|
||||
const builtInPlugins: any = {
|
||||
'app/plugins/datasource/graphite/module': graphitePlugin,
|
||||
'app/plugins/datasource/cloudwatch/module': cloudwatchPlugin,
|
||||
@ -95,6 +98,7 @@ const builtInPlugins: any = {
|
||||
'app/plugins/panel/status-history/module': statusHistoryPanel,
|
||||
'app/plugins/panel/graph/module': graphPanel,
|
||||
'app/plugins/panel/xychart/module': xyChartPanel,
|
||||
'app/plugins/panel/geomap/module': geomapPanel,
|
||||
'app/plugins/panel/dashlist/module': dashListPanel,
|
||||
'app/plugins/panel/pluginlist/module': pluginsListPanel,
|
||||
'app/plugins/panel/alertlist/module': alertListPanel,
|
||||
|
50
public/app/plugins/panel/geomap/GeomapOverlay.tsx
Normal file
50
public/app/plugins/panel/geomap/GeomapOverlay.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { stylesFactory } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
export interface OverlayProps {
|
||||
topRight?: React.ReactNode[];
|
||||
bottomLeft?: React.ReactNode[];
|
||||
}
|
||||
|
||||
export class GeomapOverlay extends PureComponent<OverlayProps> {
|
||||
style = getStyles(config.theme);
|
||||
|
||||
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}>{bottomLeft}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
overlay: css`
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 500;
|
||||
pointer-events: none;
|
||||
`,
|
||||
TR: css`
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
pointer-events: auto;
|
||||
`,
|
||||
BL: css`
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
pointer-events: auto;
|
||||
`,
|
||||
}));
|
293
public/app/plugins/panel/geomap/GeomapPanel.tsx
Normal file
293
public/app/plugins/panel/geomap/GeomapPanel.tsx
Normal file
@ -0,0 +1,293 @@
|
||||
import React, { Component } from 'react';
|
||||
import { geomapLayerRegistry } from './layers/registry';
|
||||
import { Map, View } from 'ol';
|
||||
import Attribution from 'ol/control/Attribution';
|
||||
import Zoom from 'ol/control/Zoom';
|
||||
import ScaleLine from 'ol/control/ScaleLine';
|
||||
import BaseLayer from 'ol/layer/Base';
|
||||
import { defaults as interactionDefaults } from 'ol/interaction';
|
||||
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
|
||||
|
||||
import { PanelData, MapLayerHandler, MapLayerConfig, PanelProps, GrafanaTheme } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { ControlsOptions, GeomapPanelOptions, MapViewConfig } from './types';
|
||||
import { defaultGrafanaThemedMap } from './layers/basemaps';
|
||||
import { centerPointRegistry, MapCenterID } from './view';
|
||||
import { fromLonLat } from 'ol/proj';
|
||||
import { Coordinate } from 'ol/coordinate';
|
||||
import { css } from '@emotion/css';
|
||||
import { stylesFactory } from '@grafana/ui';
|
||||
import { GeomapOverlay, OverlayProps } from './GeomapOverlay';
|
||||
import { DebugOverlay } from './components/DebugOverlay';
|
||||
import { getGlobalStyles } from './globalStyles';
|
||||
import { Global } from '@emotion/react';
|
||||
|
||||
interface MapLayerState {
|
||||
config: MapLayerConfig;
|
||||
handler: MapLayerHandler;
|
||||
layer: BaseLayer; // used to add|remove
|
||||
}
|
||||
|
||||
// Allows multiple panels to share the same view instance
|
||||
let sharedView: View | undefined = undefined;
|
||||
|
||||
type Props = PanelProps<GeomapPanelOptions>;
|
||||
export class GeomapPanel extends Component<Props> {
|
||||
globalCSS = getGlobalStyles(config.theme2);
|
||||
|
||||
map: Map;
|
||||
|
||||
basemap: BaseLayer;
|
||||
layers: MapLayerState[] = [];
|
||||
mouseWheelZoom: MouseWheelZoom;
|
||||
style = getStyles(config.theme);
|
||||
overlayProps: OverlayProps = {};
|
||||
|
||||
shouldComponentUpdate(nextProps: Props) {
|
||||
if (!this.map) {
|
||||
return true; // not yet initalized
|
||||
}
|
||||
|
||||
// Check for resize
|
||||
if (this.props.height !== nextProps.height || this.props.width !== nextProps.width) {
|
||||
this.map.updateSize();
|
||||
}
|
||||
|
||||
// External configuraiton changed
|
||||
let layersChanged = false;
|
||||
if (this.props.options !== nextProps.options) {
|
||||
layersChanged = this.optionsChanged(nextProps.options);
|
||||
}
|
||||
|
||||
// External data changed
|
||||
if (layersChanged || this.props.data !== nextProps.data) {
|
||||
this.dataChanged(nextProps.data, nextProps.options.controls.showLegend);
|
||||
}
|
||||
|
||||
return true; // always?
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the panel options change
|
||||
*/
|
||||
optionsChanged(options: GeomapPanelOptions): boolean {
|
||||
let layersChanged = false;
|
||||
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));
|
||||
}
|
||||
|
||||
if (options.controls !== oldOptions.controls) {
|
||||
console.log('Crontrols changed');
|
||||
this.initControls(options.controls ?? { showZoom: true, showAttribution: true });
|
||||
}
|
||||
|
||||
if (options.basemap !== oldOptions.basemap) {
|
||||
console.log('Basemap changed');
|
||||
this.initBasemap(options.basemap);
|
||||
layersChanged = true;
|
||||
}
|
||||
|
||||
if (options.layers !== oldOptions.layers) {
|
||||
console.log('layers changed');
|
||||
this.initLayers(options.layers ?? []);
|
||||
layersChanged = true;
|
||||
}
|
||||
return layersChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when PanelData changes (query results etc)
|
||||
*/
|
||||
dataChanged(data: PanelData, showLegend?: boolean) {
|
||||
const legends: React.ReactNode[] = [];
|
||||
for (const state of this.layers) {
|
||||
if (state.handler.update) {
|
||||
state.handler.update(data);
|
||||
}
|
||||
if (showLegend && state.handler.legend) {
|
||||
legends.push(state.handler.legend());
|
||||
}
|
||||
}
|
||||
this.overlayProps.bottomLeft = legends;
|
||||
}
|
||||
|
||||
initMapRef = (div: HTMLDivElement) => {
|
||||
if (this.map) {
|
||||
this.map.dispose();
|
||||
}
|
||||
|
||||
if (!div) {
|
||||
this.map = (undefined as unknown) as Map;
|
||||
return;
|
||||
}
|
||||
const { options } = this.props;
|
||||
this.map = new Map({
|
||||
view: this.initMapView(options.view),
|
||||
pixelRatio: 1, // or zoom?
|
||||
layers: [], // loaded explicitly below
|
||||
controls: [],
|
||||
target: div,
|
||||
interactions: interactionDefaults({
|
||||
mouseWheelZoom: false, // managed by initControls
|
||||
}),
|
||||
});
|
||||
this.mouseWheelZoom = new MouseWheelZoom();
|
||||
this.map.addInteraction(this.mouseWheelZoom);
|
||||
this.initControls(options.controls);
|
||||
this.initBasemap(options.basemap);
|
||||
this.initLayers(options.layers);
|
||||
this.dataChanged(this.props.data, options.controls.showLegend);
|
||||
this.forceUpdate(); // first render
|
||||
};
|
||||
|
||||
initBasemap(cfg: MapLayerConfig) {
|
||||
if (!cfg) {
|
||||
cfg = { type: defaultGrafanaThemedMap.id };
|
||||
}
|
||||
const item = geomapLayerRegistry.getIfExists(cfg.type) ?? defaultGrafanaThemedMap;
|
||||
const layer = item.create(this.map, cfg, config.theme2).init();
|
||||
if (this.basemap) {
|
||||
this.map.removeLayer(this.basemap);
|
||||
this.basemap.dispose();
|
||||
}
|
||||
this.basemap = layer;
|
||||
this.map.getLayers().insertAt(0, this.basemap);
|
||||
}
|
||||
|
||||
initLayers(layers: MapLayerConfig[]) {
|
||||
// 1st remove existing layers
|
||||
for (const state of this.layers) {
|
||||
this.map.removeLayer(state.layer);
|
||||
state.layer.dispose();
|
||||
}
|
||||
|
||||
if (!layers) {
|
||||
layers = [];
|
||||
}
|
||||
|
||||
this.layers = [];
|
||||
for (const overlay of layers) {
|
||||
const item = geomapLayerRegistry.getIfExists(overlay.type);
|
||||
if (!item) {
|
||||
console.warn('unknown layer type: ', overlay);
|
||||
continue; // TODO -- panel warning?
|
||||
}
|
||||
|
||||
const handler = item.create(this.map, overlay, config.theme2);
|
||||
const layer = handler.init();
|
||||
this.map.addLayer(layer);
|
||||
this.layers.push({
|
||||
config: overlay,
|
||||
layer,
|
||||
handler,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
initMapView(config: MapViewConfig): View {
|
||||
let view = new View({
|
||||
center: [0, 0],
|
||||
zoom: 1,
|
||||
});
|
||||
|
||||
// With shared views, all panels use the same view instance
|
||||
if (config.shared) {
|
||||
if (!sharedView) {
|
||||
sharedView = view;
|
||||
} else {
|
||||
view = sharedView;
|
||||
}
|
||||
}
|
||||
|
||||
const v = centerPointRegistry.getIfExists(config.center.id);
|
||||
if (v) {
|
||||
let coord: Coordinate | undefined = undefined;
|
||||
if (v.lat == null) {
|
||||
if (v.id === MapCenterID.Coordinates) {
|
||||
const center = config.center ?? {};
|
||||
coord = [center.lon ?? 0, center.lat ?? 0];
|
||||
} 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) {
|
||||
view.setZoom(config.zoom);
|
||||
}
|
||||
return view;
|
||||
}
|
||||
|
||||
initControls(options: ControlsOptions) {
|
||||
this.map.getControls().clear();
|
||||
|
||||
if (options.showZoom) {
|
||||
this.map.addControl(new Zoom());
|
||||
}
|
||||
|
||||
if (options.showScale) {
|
||||
this.map.addControl(
|
||||
new ScaleLine({
|
||||
units: options.scaleUnits,
|
||||
minWidth: 100,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.mouseWheelZoom.setActive(Boolean(options.mouseWheelZoom));
|
||||
|
||||
if (options.showAttribution) {
|
||||
this.map.addControl(new Attribution({ collapsed: true, collapsible: true }));
|
||||
}
|
||||
|
||||
// Update the react overlays
|
||||
const overlayProps: OverlayProps = {};
|
||||
if (options.showDebug) {
|
||||
overlayProps.topRight = [<DebugOverlay key="debug" map={this.map} />];
|
||||
}
|
||||
|
||||
this.overlayProps = overlayProps;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Global styles={this.globalCSS} />
|
||||
<div className={this.style.wrap}>
|
||||
<div className={this.style.map} ref={this.initMapRef}></div>
|
||||
<GeomapOverlay {...this.overlayProps} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
wrap: css`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`,
|
||||
map: css`
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`,
|
||||
}));
|
3
public/app/plugins/panel/geomap/README.md
Normal file
3
public/app/plugins/panel/geomap/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# Geomap Panel - Native Plugin
|
||||
|
||||
The Geomap is **included** with Grafana.
|
72
public/app/plugins/panel/geomap/components/DebugOverlay.tsx
Normal file
72
public/app/plugins/panel/geomap/components/DebugOverlay.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Map } from 'ol';
|
||||
import { transform } from 'ol/proj';
|
||||
import { stylesFactory } from '@grafana/ui';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { css } from '@emotion/css';
|
||||
import { config } from 'app/core/config';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { Coordinate } from 'ol/coordinate';
|
||||
|
||||
interface Props {
|
||||
map: Map;
|
||||
}
|
||||
|
||||
interface State {
|
||||
zoom?: number;
|
||||
center: Coordinate;
|
||||
}
|
||||
|
||||
export class DebugOverlay extends PureComponent<Props, State> {
|
||||
style = getStyles(config.theme);
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { zoom: 0, center: [0, 0] };
|
||||
}
|
||||
|
||||
updateViewState = () => {
|
||||
const view = this.props.map.getView();
|
||||
this.setState({
|
||||
zoom: view.getZoom(),
|
||||
center: transform(view.getCenter()!, view.getProjection(), 'EPSG:4326'),
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.map.on('moveend', this.updateViewState);
|
||||
this.updateViewState();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { zoom, center } = this.state;
|
||||
|
||||
return (
|
||||
<div className={this.style.infoWrap}>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Zoom:</th>
|
||||
<td>{zoom?.toFixed(1)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Center: </th>
|
||||
<td>
|
||||
{center[0].toFixed(5)}, {center[1].toFixed(5)}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
infoWrap: css`
|
||||
color: ${theme.colors.text};
|
||||
background: ${tinycolor(theme.colors.panelBg).setAlpha(0.7).toString()};
|
||||
border-radius: 2px;
|
||||
padding: 8px;
|
||||
`,
|
||||
}));
|
77
public/app/plugins/panel/geomap/components/NumberInput.tsx
Normal file
77
public/app/plugins/panel/geomap/components/NumberInput.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Input } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
value?: number;
|
||||
placeholder?: string;
|
||||
autoFocus?: boolean;
|
||||
onChange: (number?: number) => void;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
interface State {
|
||||
text: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an Input field that will call `onChange` for blur and enter
|
||||
*/
|
||||
export class NumberInput extends PureComponent<Props, State> {
|
||||
state: State = { text: '' };
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({
|
||||
text: isNaN(this.props.value!) ? '' : `${this.props.value}`,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(oldProps: Props) {
|
||||
if (this.props.value !== oldProps.value) {
|
||||
this.setState({
|
||||
text: isNaN(this.props.value!) ? '' : `${this.props.value}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onBlur = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
let value: number | undefined = undefined;
|
||||
const txt = e.currentTarget.value;
|
||||
if (txt && !isNaN(e.currentTarget.valueAsNumber)) {
|
||||
value = e.currentTarget.valueAsNumber;
|
||||
}
|
||||
this.props.onChange(value);
|
||||
};
|
||||
|
||||
onChange = (e: React.FocusEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
text: e.currentTarget.value,
|
||||
});
|
||||
};
|
||||
|
||||
onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.onBlur(e as any);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { placeholder } = this.props;
|
||||
const { text } = this.state;
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
min={this.props.min}
|
||||
max={this.props.max}
|
||||
step={this.props.step}
|
||||
autoFocus={this.props.autoFocus}
|
||||
value={text}
|
||||
onChange={this.onChange}
|
||||
onBlur={this.onBlur}
|
||||
onKeyPress={this.onKeyPress}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
96
public/app/plugins/panel/geomap/components/SimpleLegend.tsx
Normal file
96
public/app/plugins/panel/geomap/components/SimpleLegend.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { stylesFactory } from '@grafana/ui';
|
||||
import { FieldType, formattedValueToString, GrafanaTheme, PanelData, ThresholdsConfig } from '@grafana/data';
|
||||
import { css } from '@emotion/css';
|
||||
import { config } from 'app/core/config';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
interface Props {
|
||||
txt: string;
|
||||
data?: PanelData;
|
||||
}
|
||||
|
||||
interface State {}
|
||||
|
||||
export class SimpleLegend extends PureComponent<Props, State> {
|
||||
style = getStyles(config.theme);
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
render() {
|
||||
let fmt = (v: any) => `${v}`;
|
||||
let thresholds: ThresholdsConfig | undefined;
|
||||
const series = this.props.data?.series;
|
||||
if (series) {
|
||||
for (const frame of series) {
|
||||
for (const field of frame.fields) {
|
||||
if (field.type === FieldType.number && field.config.thresholds) {
|
||||
thresholds = field.config.thresholds;
|
||||
fmt = (v: any) => `${formattedValueToString(field.display!(v))}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={this.style.infoWrap}>
|
||||
<div>{this.props.txt}</div>
|
||||
{thresholds && (
|
||||
<div className={this.style.legend}>
|
||||
{thresholds.steps.map((step, idx) => {
|
||||
const next = thresholds!.steps[idx + 1];
|
||||
let info = <span>?</span>;
|
||||
if (idx === 0) {
|
||||
info = <span>< {fmt(next.value)}</span>;
|
||||
} else if (next) {
|
||||
info = (
|
||||
<span>
|
||||
{fmt(step.value)} - {fmt(next.value)}
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
info = <span>{fmt(step.value)} +</span>;
|
||||
}
|
||||
return (
|
||||
<div key={`${idx}/${step.value}`} className={this.style.legendItem}>
|
||||
<i style={{ background: config.theme2.visualization.getColorByName(step.color) }}></i>
|
||||
{info}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
infoWrap: css`
|
||||
color: ${theme.colors.text};
|
||||
background: ${tinycolor(theme.colors.panelBg).setAlpha(0.7).toString()};
|
||||
border-radius: 2px;
|
||||
padding: 8px;
|
||||
`,
|
||||
legend: css`
|
||||
line-height: 18px;
|
||||
color: #555;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
i {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
float: left;
|
||||
margin-right: 8px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
`,
|
||||
legendItem: css`
|
||||
white-space: nowrap;
|
||||
`,
|
||||
}));
|
12
public/app/plugins/panel/geomap/editor/BaseLayerEditor.tsx
Normal file
12
public/app/plugins/panel/geomap/editor/BaseLayerEditor.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React, { FC } from 'react';
|
||||
import { StandardEditorProps, MapLayerConfig } from '@grafana/data';
|
||||
import { GeomapPanelOptions } from '../types';
|
||||
import { LayerEditor } from './LayerEditor';
|
||||
|
||||
export const BaseLayerEditor: FC<StandardEditorProps<MapLayerConfig, any, GeomapPanelOptions>> = ({
|
||||
value,
|
||||
onChange,
|
||||
context,
|
||||
}) => {
|
||||
return <LayerEditor config={value} data={context.data} onChange={onChange} filter={(v) => Boolean(v.isBaseMap)} />;
|
||||
};
|
23
public/app/plugins/panel/geomap/editor/DataLayersEditor.tsx
Normal file
23
public/app/plugins/panel/geomap/editor/DataLayersEditor.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React, { FC } from 'react';
|
||||
import { StandardEditorProps, MapLayerConfig } from '@grafana/data';
|
||||
import { GeomapPanelOptions } from '../types';
|
||||
import { LayerEditor } from './LayerEditor';
|
||||
|
||||
// For now this supports a *single* data layer -- eventually we should support more than one
|
||||
export const DataLayersEditor: FC<StandardEditorProps<MapLayerConfig[], any, GeomapPanelOptions>> = ({
|
||||
value,
|
||||
onChange,
|
||||
context,
|
||||
}) => {
|
||||
return (
|
||||
<LayerEditor
|
||||
config={value?.length ? value[0] : undefined}
|
||||
data={context.data}
|
||||
onChange={(cfg) => {
|
||||
console.log('Change overlays:', cfg);
|
||||
onChange([cfg]);
|
||||
}}
|
||||
filter={(v) => !v.isBaseMap}
|
||||
/>
|
||||
);
|
||||
};
|
111
public/app/plugins/panel/geomap/editor/LayerEditor.tsx
Normal file
111
public/app/plugins/panel/geomap/editor/LayerEditor.tsx
Normal file
@ -0,0 +1,111 @@
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { Select } from '@grafana/ui';
|
||||
import {
|
||||
MapLayerConfig,
|
||||
DataFrame,
|
||||
MapLayerRegistryItem,
|
||||
PanelOptionsEditorBuilder,
|
||||
StandardEditorContext,
|
||||
} from '@grafana/data';
|
||||
import { geomapLayerRegistry } from '../layers/registry';
|
||||
import { defaultGrafanaThemedMap } from '../layers/basemaps';
|
||||
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
|
||||
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
|
||||
import { fillOptionsPaneItems } from 'app/features/dashboard/components/PanelEditor/getVizualizationOptions';
|
||||
|
||||
export interface LayerEditorProps<TConfig = any> {
|
||||
config?: MapLayerConfig<TConfig>;
|
||||
data: DataFrame[]; // All results
|
||||
onChange: (config: MapLayerConfig<TConfig>) => void;
|
||||
filter: (item: MapLayerRegistryItem) => boolean;
|
||||
}
|
||||
|
||||
export const LayerEditor: FC<LayerEditorProps> = ({ config, onChange, data, filter }) => {
|
||||
// all basemaps
|
||||
const layerTypes = useMemo(() => {
|
||||
return geomapLayerRegistry.selectOptions(
|
||||
config?.type // the selected value
|
||||
? [config.type] // as an array
|
||||
: [defaultGrafanaThemedMap.id],
|
||||
filter
|
||||
);
|
||||
}, [config?.type, filter]);
|
||||
|
||||
// The options change with each layer type
|
||||
const optionsEditorBuilder = useMemo(() => {
|
||||
const layer = geomapLayerRegistry.getIfExists(config?.type);
|
||||
if (!layer || !layer.registerOptionsUI) {
|
||||
return null;
|
||||
}
|
||||
const builder = new PanelOptionsEditorBuilder();
|
||||
layer.registerOptionsUI(builder);
|
||||
return builder;
|
||||
}, [config?.type]);
|
||||
|
||||
// The react componnets
|
||||
const layerOptions = useMemo(() => {
|
||||
const layer = geomapLayerRegistry.getIfExists(config?.type);
|
||||
if (!optionsEditorBuilder || !layer) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const category = new OptionsPaneCategoryDescriptor({
|
||||
id: 'Layer config',
|
||||
title: 'Layer config',
|
||||
});
|
||||
|
||||
const context: StandardEditorContext<any> = {
|
||||
data,
|
||||
options: config?.config,
|
||||
};
|
||||
|
||||
const currentConfig = { ...layer.defaultOptions, ...config?.config };
|
||||
const reg = optionsEditorBuilder.getRegistry();
|
||||
|
||||
// Load the options into categories
|
||||
fillOptionsPaneItems(
|
||||
reg.list(),
|
||||
|
||||
// Always use the same category
|
||||
(categoryNames) => category,
|
||||
|
||||
// Custom upate function
|
||||
(path: string, value: any) => {
|
||||
onChange({
|
||||
...config,
|
||||
config: setOptionImmutably(currentConfig, path, value),
|
||||
} as MapLayerConfig);
|
||||
},
|
||||
context
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
{category.items.map((item) => item.render())}
|
||||
</>
|
||||
);
|
||||
}, [optionsEditorBuilder, onChange, data, config]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
options={layerTypes.options}
|
||||
value={layerTypes.current}
|
||||
onChange={(v) => {
|
||||
const layer = geomapLayerRegistry.getIfExists(v.value);
|
||||
if (!layer) {
|
||||
console.warn('layer does not exist', v);
|
||||
return;
|
||||
}
|
||||
onChange({
|
||||
type: layer.id,
|
||||
config: layer.defaultOptions, // clone?
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
{layerOptions}
|
||||
</div>
|
||||
);
|
||||
};
|
85
public/app/plugins/panel/geomap/editor/MapCenterEditor.tsx
Normal file
85
public/app/plugins/panel/geomap/editor/MapCenterEditor.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import React, { FC, useMemo } from 'react';
|
||||
import { GrafanaTheme, StandardEditorProps } from '@grafana/data';
|
||||
import { Select, stylesFactory, useStyles } from '@grafana/ui';
|
||||
import { GeomapPanelOptions, MapCenterConfig } from '../types';
|
||||
import { centerPointRegistry, MapCenterID } from '../view';
|
||||
import { css } from '@emotion/css';
|
||||
import { NumberInput } from '../components/NumberInput';
|
||||
|
||||
export const MapCenterEditor: FC<StandardEditorProps<MapCenterConfig, any, GeomapPanelOptions>> = ({
|
||||
value,
|
||||
onChange,
|
||||
context,
|
||||
}) => {
|
||||
const style = useStyles(getStyles);
|
||||
|
||||
const views = useMemo(() => {
|
||||
const ids: string[] = [];
|
||||
if (value?.id) {
|
||||
ids.push(value.id);
|
||||
} else {
|
||||
ids.push(centerPointRegistry.list()[0].id);
|
||||
}
|
||||
return centerPointRegistry.selectOptions(ids);
|
||||
}, [value?.id]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Select
|
||||
options={views.options}
|
||||
value={views.current}
|
||||
onChange={(v) => {
|
||||
onChange({
|
||||
id: v.value!,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{value?.id === MapCenterID.Coordinates && (
|
||||
<div>
|
||||
<table className={style.table}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th className={style.half}>Latitude</th>
|
||||
<th className={style.half}>Longitude</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<NumberInput
|
||||
value={value.lat}
|
||||
min={-90}
|
||||
max={90}
|
||||
placeholder="0"
|
||||
onChange={(v) => {
|
||||
onChange({ ...value, lat: v });
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<NumberInput
|
||||
value={value.lon}
|
||||
min={-180}
|
||||
max={180}
|
||||
placeholder="0"
|
||||
onChange={(v) => {
|
||||
onChange({ ...value, lon: v });
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||
table: css`
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
`,
|
||||
half: css`
|
||||
width: 50%;
|
||||
`,
|
||||
}));
|
18
public/app/plugins/panel/geomap/editor/MapZoomEditor.tsx
Normal file
18
public/app/plugins/panel/geomap/editor/MapZoomEditor.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React, { FC } from 'react';
|
||||
import { StandardEditorProps } from '@grafana/data';
|
||||
import { GeomapPanelOptions } from '../types';
|
||||
import { NumberInput } from '../components/NumberInput';
|
||||
|
||||
export const MapZoomEditor: FC<StandardEditorProps<number | undefined, any, GeomapPanelOptions>> = ({
|
||||
value,
|
||||
onChange,
|
||||
context,
|
||||
}) => {
|
||||
// TODO:
|
||||
// Somehow use context to get the current map and listen to zoom changes
|
||||
return (
|
||||
<div>
|
||||
<NumberInput value={value} min={1} max={30} onChange={onChange} />
|
||||
</div>
|
||||
);
|
||||
};
|
84
public/app/plugins/panel/geomap/globalStyles.ts
Normal file
84
public/app/plugins/panel/geomap/globalStyles.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import { css } from '@emotion/react';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
import 'ol/ol.css';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
/**
|
||||
* Will be loaded *after* the css above
|
||||
*/
|
||||
export function getGlobalStyles(theme: GrafanaTheme2) {
|
||||
// NOTE: this works with
|
||||
// node_modules/ol/ol.css
|
||||
// use !important;
|
||||
// This file keeps the rules
|
||||
|
||||
// .ol-box {
|
||||
// border: 2px solid blue;
|
||||
// }
|
||||
|
||||
// .ol-scale-step-marker {
|
||||
// background-color: #000000;
|
||||
// }
|
||||
// .ol-scale-step-text {
|
||||
// color: #000000;
|
||||
// text-shadow: -2px 0 #FFFFFF, 0 2px #FFFFFF, 2px 0 #FFFFFF, 0 -2px #FFFFFF;
|
||||
// }
|
||||
// .ol-scale-text {
|
||||
// color: #000000;
|
||||
// text-shadow: -2px 0 #FFFFFF, 0 2px #FFFFFF, 2px 0 #FFFFFF, 0 -2px #FFFFFF;
|
||||
// }
|
||||
// .ol-scale-singlebar {
|
||||
// border: 1px solid black;
|
||||
// }
|
||||
// .ol-viewport, .ol-unselectable {
|
||||
// -webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||
// }
|
||||
|
||||
// .ol-overviewmap .ol-overviewmap-map {
|
||||
// border: 1px solid #7b98bc;
|
||||
// }
|
||||
// .ol-overviewmap:not(.ol-collapsed) {
|
||||
// background: rgba(255,255,255,0.8);
|
||||
// }
|
||||
// .ol-overviewmap-box {
|
||||
// border: 2px dotted rgba(0,60,136,0.7);
|
||||
// }
|
||||
|
||||
const bg = tinycolor(theme.v1.colors.panelBg);
|
||||
const button = tinycolor(theme.colors.secondary.main);
|
||||
return css`
|
||||
.ol-scale-line {
|
||||
background: ${bg.setAlpha(0.7).toRgbString()}; // rgba(0,60,136,0.3);
|
||||
}
|
||||
.ol-scale-line-inner {
|
||||
border: 1px solid ${theme.colors.text.primary}; // #eee;
|
||||
border-top: 0px;
|
||||
color: ${theme.colors.text.primary}; // #eee;
|
||||
}
|
||||
.ol-control {
|
||||
background-color: ${bg.setAlpha(0.4).toRgbString()}; //rgba(255,255,255,0.4);
|
||||
}
|
||||
.ol-control:hover {
|
||||
background-color: ${bg.setAlpha(0.6).toRgbString()}; // rgba(255,255,255,0.6);
|
||||
}
|
||||
.ol-control button {
|
||||
color: ${bg.setAlpha(0.8).toRgbString()}; // white;
|
||||
background-color: ${button.setAlpha(0.5).toRgbString()}; // rgba(0,60,136,0.5);
|
||||
}
|
||||
.ol-control button:hover {
|
||||
background-color: ${button.setAlpha(0.7).toRgbString()}; // rgba(0,60,136,0.7);
|
||||
}
|
||||
.ol-control button:focus {
|
||||
// same as button
|
||||
background-color: ${button.setAlpha(0.5).toRgbString()}; // rgba(0,60,136,0.5);
|
||||
}
|
||||
.ol-attribution ul {
|
||||
color: ${theme.colors.text.primary}; // #000;
|
||||
text-shadow: 0 0 0px #fff; // removes internal styling!
|
||||
}
|
||||
.ol-attribution:not(.ol-collapsed) {
|
||||
background-color: ${bg.setAlpha(0.8).toRgbString()}; // rgba(255,255,255,0.8);
|
||||
}
|
||||
`;
|
||||
}
|
1
public/app/plugins/panel/geomap/img/icn-geomap.svg
Normal file
1
public/app/plugins/panel/geomap/img/icn-geomap.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 79.8 82.08"><defs><style>.cls-1{fill:#84aff1;}.cls-2{fill:#3865ab;}.cls-3{fill:#fff;}.cls-4{fill:url(#linear-gradient);}</style><linearGradient id="linear-gradient" x1="23.91" y1="19.31" x2="55.89" y2="19.31" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f2cc0c"/><stop offset="1" stop-color="#ff9830"/></linearGradient></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M54,30.13,39.9,44.27h0V82.08l20-10V16.88A19.85,19.85,0,0,1,54,30.13Z"/><path class="cls-2" d="M25.76,30.13A19.89,19.89,0,0,1,20,16.69V72.11l20,10V44.27Z"/><path class="cls-1" d="M20,72.11,1.45,81.36A1,1,0,0,1,0,80.47V26.18a1,1,0,0,1,.55-.89L20,15.6Z"/><path class="cls-2" d="M78.35,81.36l-18.5-9.25V15.6l19.4,9.69a1,1,0,0,1,.55.89V80.47A1,1,0,0,1,78.35,81.36Z"/><path class="cls-3" d="M68.37,37.9a2.26,2.26,0,0,0,.79.5,2.39,2.39,0,0,0,1.82,0,2.17,2.17,0,0,0,.79-.5,2.55,2.55,0,0,0,.51-.79,2.42,2.42,0,0,0-.51-2.62A2.71,2.71,0,0,0,71,34a2.22,2.22,0,0,0-1.82,0,2.71,2.71,0,0,0-.79.5,2.42,2.42,0,0,0-.51,2.62A2.55,2.55,0,0,0,68.37,37.9Z"/><path class="cls-3" d="M68.37,67.57a2.44,2.44,0,0,0,.79.51,2.39,2.39,0,0,0,1.82,0,2.23,2.23,0,0,0,1.3-1.3,2.41,2.41,0,0,0-.51-2.61,3,3,0,0,0-.79-.51,2.3,2.3,0,0,0-1.82,0,3,3,0,0,0-.79.51,2.41,2.41,0,0,0-.51,2.61A2.34,2.34,0,0,0,68.37,67.57Z"/><path class="cls-3" d="M48.17,47.39a2.52,2.52,0,0,0,.79.5,2.42,2.42,0,0,0,1.83,0,2.52,2.52,0,0,0,.79-.5,2.3,2.3,0,0,0,.5-.8,2.38,2.38,0,0,0-.5-2.61,2.71,2.71,0,0,0-.79-.5,2.24,2.24,0,0,0-1.83,0,2.85,2.85,0,0,0-.79.5,2.38,2.38,0,0,0-.5,2.61A2.3,2.3,0,0,0,48.17,47.39Z"/><path class="cls-3" d="M28,67.57a2.55,2.55,0,0,0,.79.51,2.39,2.39,0,0,0,1.82,0,2.23,2.23,0,0,0,1.3-1.3,2.41,2.41,0,0,0-.51-2.61,3,3,0,0,0-.79-.51,2.3,2.3,0,0,0-1.82,0,3,3,0,0,0-.79.51,2.38,2.38,0,0,0-.5,2.61A2.17,2.17,0,0,0,28,67.57Z"/><path class="cls-3" d="M8.27,37.9a2.35,2.35,0,0,0,.79.5,2.42,2.42,0,0,0,1.83,0,2.13,2.13,0,0,0,1.29-1.29,2.4,2.4,0,0,0-.5-2.62,2.85,2.85,0,0,0-.79-.5,2.24,2.24,0,0,0-1.83,0,2.85,2.85,0,0,0-.79.5,2.4,2.4,0,0,0-.5,2.62A2.35,2.35,0,0,0,8.27,37.9Z"/><path class="cls-4" d="M51.21,4.68A16,16,0,0,0,28.59,27.3L39.9,38.61,51.21,27.3a16,16,0,0,0,0-22.62ZM45.38,17.17a5.38,5.38,0,0,1-3.21,3.21,5.9,5.9,0,0,1-4.52,0,6,6,0,0,1-2-1.25,6,6,0,0,1-1.25-2,6,6,0,0,1,1.25-6.48,6.79,6.79,0,0,1,2-1.25,5.56,5.56,0,0,1,4.52,0,6.74,6.74,0,0,1,2,1.25A5.94,5.94,0,0,1,45.38,17.17Z"/></g></g></svg>
|
After Width: | Height: | Size: 2.4 KiB |
78
public/app/plugins/panel/geomap/layers/basemaps/carto.ts
Normal file
78
public/app/plugins/panel/geomap/layers/basemaps/carto.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { MapLayerRegistryItem, MapLayerConfig, GrafanaTheme2 } from '@grafana/data';
|
||||
import Map from 'ol/Map';
|
||||
import XYZ from 'ol/source/XYZ';
|
||||
import TileLayer from 'ol/layer/Tile';
|
||||
|
||||
// https://carto.com/help/building-maps/basemap-list/
|
||||
|
||||
export enum LayerTheme {
|
||||
Auto = 'auto',
|
||||
Light = 'light',
|
||||
Dark = 'dark',
|
||||
}
|
||||
|
||||
export interface CartoConfig {
|
||||
theme?: LayerTheme;
|
||||
showLabels?: boolean;
|
||||
}
|
||||
|
||||
export const defaultCartoConfig: CartoConfig = {
|
||||
theme: LayerTheme.Auto,
|
||||
showLabels: true,
|
||||
};
|
||||
|
||||
export const carto: MapLayerRegistryItem<CartoConfig> = {
|
||||
id: 'carto',
|
||||
name: 'CARTO reference map',
|
||||
isBaseMap: true,
|
||||
defaultOptions: defaultCartoConfig,
|
||||
|
||||
/**
|
||||
* Function that configures transformation and returns a transformer
|
||||
* @param options
|
||||
*/
|
||||
create: (map: Map, options: MapLayerConfig<CartoConfig>, theme: GrafanaTheme2) => ({
|
||||
init: () => {
|
||||
const cfg = { ...defaultCartoConfig, ...options.config };
|
||||
let style = cfg.theme as string;
|
||||
if (!style || style === LayerTheme.Auto) {
|
||||
style = theme.isDark ? 'dark' : 'light';
|
||||
}
|
||||
if (cfg.showLabels) {
|
||||
style += '_all';
|
||||
} else {
|
||||
style += '_nolabels';
|
||||
}
|
||||
return new TileLayer({
|
||||
source: new XYZ({
|
||||
attributions: `<a href="https://carto.com/attribution/">© CARTO</a>`,
|
||||
url: `https://{1-4}.basemaps.cartocdn.com/${style}/{z}/{x}/{y}.png`,
|
||||
}),
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
registerOptionsUI: (builder) => {
|
||||
builder
|
||||
.addRadio({
|
||||
path: 'theme',
|
||||
name: 'Theme',
|
||||
settings: {
|
||||
options: [
|
||||
{ value: LayerTheme.Auto, label: 'Auto', description: 'Match grafana theme' },
|
||||
{ value: LayerTheme.Light, label: 'Light' },
|
||||
{ value: LayerTheme.Dark, label: 'Dark' },
|
||||
],
|
||||
},
|
||||
defaultValue: defaultCartoConfig.theme!,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'showLabels',
|
||||
name: 'Show labels',
|
||||
description: '',
|
||||
defaultValue: defaultCartoConfig.showLabels,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const cartoLayers = [carto];
|
107
public/app/plugins/panel/geomap/layers/basemaps/esri.ts
Normal file
107
public/app/plugins/panel/geomap/layers/basemaps/esri.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import { MapLayerRegistryItem, MapLayerConfig, GrafanaTheme2, RegistryItem, Registry } from '@grafana/data';
|
||||
import Map from 'ol/Map';
|
||||
import { xyzTiles, defaultXYZConfig, XYZConfig } from './generic';
|
||||
|
||||
interface PublicServiceItem extends RegistryItem {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const CUSTOM_SERVICE = 'custom';
|
||||
const DEFAULT_SERVICE = 'streets';
|
||||
|
||||
export const publicServiceRegistry = new Registry<PublicServiceItem>(() => [
|
||||
{
|
||||
id: DEFAULT_SERVICE,
|
||||
name: 'World Street Map',
|
||||
slug: 'World_Street_Map',
|
||||
},
|
||||
{
|
||||
id: 'world-imagery',
|
||||
name: 'World Imagery',
|
||||
slug: 'World_Imagery',
|
||||
},
|
||||
{
|
||||
id: 'world-physical',
|
||||
name: 'World Physical',
|
||||
slug: 'World_Physical_Map',
|
||||
},
|
||||
{
|
||||
id: 'topo',
|
||||
name: 'Topographic',
|
||||
slug: 'World_Topo_Map',
|
||||
},
|
||||
{
|
||||
id: 'usa-topo',
|
||||
name: 'USA Topographic',
|
||||
slug: 'USA_Topo_Maps',
|
||||
},
|
||||
{
|
||||
id: 'ocean',
|
||||
name: 'World Ocean',
|
||||
slug: 'Ocean/World_Ocean_Base',
|
||||
},
|
||||
{
|
||||
id: CUSTOM_SERVICE,
|
||||
name: 'Custom MapServer',
|
||||
description: 'Use a custom MapServer with pre-cached values',
|
||||
slug: '',
|
||||
},
|
||||
]);
|
||||
|
||||
export interface ESRIXYZConfig extends XYZConfig {
|
||||
server: string;
|
||||
}
|
||||
|
||||
export const esriXYZTiles: MapLayerRegistryItem<ESRIXYZConfig> = {
|
||||
id: 'esri-xyz',
|
||||
name: 'ArcGIS MapServer',
|
||||
isBaseMap: true,
|
||||
|
||||
create: (map: Map, options: MapLayerConfig<ESRIXYZConfig>, theme: GrafanaTheme2) => ({
|
||||
init: () => {
|
||||
const cfg = { ...options.config };
|
||||
const svc = publicServiceRegistry.getIfExists(cfg.server ?? DEFAULT_SERVICE)!;
|
||||
if (svc.id !== CUSTOM_SERVICE) {
|
||||
const base = 'https://services.arcgisonline.com/ArcGIS/rest/services/';
|
||||
cfg.url = `${base}${svc.slug}/MapServer/tile/{z}/{y}/{x}`;
|
||||
cfg.attribution = `Tiles © <a href="${base}${svc.slug}/MapServer">ArcGIS</a>`;
|
||||
}
|
||||
// reuse the standard XYZ tile logic
|
||||
return xyzTiles.create(map, { ...options, config: cfg as XYZConfig }, theme).init();
|
||||
},
|
||||
}),
|
||||
|
||||
registerOptionsUI: (builder) => {
|
||||
builder
|
||||
.addSelect({
|
||||
path: 'server',
|
||||
name: 'Server instance',
|
||||
settings: {
|
||||
options: publicServiceRegistry.selectOptions().options,
|
||||
},
|
||||
})
|
||||
.addTextInput({
|
||||
path: 'url',
|
||||
name: 'URL template',
|
||||
description: 'Must include {x}, {y} or {-y}, and {z} placeholders',
|
||||
settings: {
|
||||
placeholder: defaultXYZConfig.url,
|
||||
},
|
||||
showIf: (cfg) => cfg.server === CUSTOM_SERVICE,
|
||||
})
|
||||
.addTextInput({
|
||||
path: 'attribution',
|
||||
name: 'Attribution',
|
||||
settings: {
|
||||
placeholder: defaultXYZConfig.attribution,
|
||||
},
|
||||
showIf: (cfg) => cfg.server === CUSTOM_SERVICE,
|
||||
});
|
||||
},
|
||||
|
||||
defaultOptions: {
|
||||
server: DEFAULT_SERVICE,
|
||||
},
|
||||
};
|
||||
|
||||
export const esriLayers = [esriXYZTiles];
|
62
public/app/plugins/panel/geomap/layers/basemaps/generic.ts
Normal file
62
public/app/plugins/panel/geomap/layers/basemaps/generic.ts
Normal file
@ -0,0 +1,62 @@
|
||||
import { MapLayerRegistryItem, MapLayerConfig, GrafanaTheme2 } from '@grafana/data';
|
||||
import Map from 'ol/Map';
|
||||
import XYZ from 'ol/source/XYZ';
|
||||
import TileLayer from 'ol/layer/Tile';
|
||||
|
||||
export interface XYZConfig {
|
||||
url: string;
|
||||
attribution: string;
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
}
|
||||
|
||||
const sampleURL = 'https://services.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer';
|
||||
export const defaultXYZConfig: XYZConfig = {
|
||||
url: sampleURL + '/tile/{z}/{y}/{x}',
|
||||
attribution: `Tiles © <a href="${sampleURL}">ArcGIS</a>`,
|
||||
};
|
||||
|
||||
export const xyzTiles: MapLayerRegistryItem<XYZConfig> = {
|
||||
id: 'xyz',
|
||||
name: 'XYZ Tile layer',
|
||||
isBaseMap: true,
|
||||
|
||||
create: (map: Map, options: MapLayerConfig<XYZConfig>, theme: GrafanaTheme2) => ({
|
||||
init: () => {
|
||||
const cfg = { ...options.config };
|
||||
if (!cfg.url) {
|
||||
cfg.url = defaultXYZConfig.url;
|
||||
cfg.attribution = cfg.attribution ?? defaultXYZConfig.attribution;
|
||||
}
|
||||
return new TileLayer({
|
||||
source: new XYZ({
|
||||
url: cfg.url,
|
||||
attributions: cfg.attribution, // singular?
|
||||
}),
|
||||
minZoom: cfg.minZoom,
|
||||
maxZoom: cfg.maxZoom,
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
registerOptionsUI: (builder) => {
|
||||
builder
|
||||
.addTextInput({
|
||||
path: 'url',
|
||||
name: 'URL template',
|
||||
description: 'Must include {x}, {y} or {-y}, and {z} placeholders',
|
||||
settings: {
|
||||
placeholder: defaultXYZConfig.url,
|
||||
},
|
||||
})
|
||||
.addTextInput({
|
||||
path: 'attribution',
|
||||
name: 'Attribution',
|
||||
settings: {
|
||||
placeholder: defaultXYZConfig.attribution,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const genericLayers = [xyzTiles];
|
22
public/app/plugins/panel/geomap/layers/basemaps/index.ts
Normal file
22
public/app/plugins/panel/geomap/layers/basemaps/index.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { cartoLayers, carto } from './carto';
|
||||
import { esriLayers } from './esri';
|
||||
import { genericLayers } from './generic';
|
||||
import { osmLayers } from './osm';
|
||||
|
||||
// For now just use carto
|
||||
export const defaultGrafanaThemedMap = {
|
||||
...carto,
|
||||
id: 'default',
|
||||
name: 'Default base layer',
|
||||
};
|
||||
|
||||
/**
|
||||
* Registry for layer handlers
|
||||
*/
|
||||
export const basemapLayers = [
|
||||
defaultGrafanaThemedMap,
|
||||
...osmLayers,
|
||||
...cartoLayers,
|
||||
...esriLayers, // keep formatting
|
||||
...genericLayers,
|
||||
];
|
24
public/app/plugins/panel/geomap/layers/basemaps/osm.ts
Normal file
24
public/app/plugins/panel/geomap/layers/basemaps/osm.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { MapLayerRegistryItem, MapLayerConfig } from '@grafana/data';
|
||||
import Map from 'ol/Map';
|
||||
import OSM from 'ol/source/OSM';
|
||||
import TileLayer from 'ol/layer/Tile';
|
||||
|
||||
const standard: MapLayerRegistryItem = {
|
||||
id: 'osm-standard',
|
||||
name: 'Open Street Map',
|
||||
isBaseMap: true,
|
||||
|
||||
/**
|
||||
* Function that configures transformation and returns a transformer
|
||||
* @param options
|
||||
*/
|
||||
create: (map: Map, options: MapLayerConfig) => ({
|
||||
init: () => {
|
||||
return new TileLayer({
|
||||
source: new OSM(),
|
||||
});
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
export const osmLayers = [standard];
|
59
public/app/plugins/panel/geomap/layers/data/geojsonMapper.ts
Normal file
59
public/app/plugins/panel/geomap/layers/data/geojsonMapper.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { MapLayerRegistryItem, MapLayerConfig, MapLayerHandler, PanelData, GrafanaTheme2 } from '@grafana/data';
|
||||
import Map from 'ol/Map';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
import VectorSource from 'ol/source/Vector';
|
||||
import GeoJSON from 'ol/format/GeoJSON';
|
||||
|
||||
export interface GeoJSONMapperConfig {
|
||||
// URL for a geojson file
|
||||
src?: string;
|
||||
|
||||
// Field name that will map to each featureId
|
||||
idField?: string;
|
||||
|
||||
// Field to use that will set color
|
||||
valueField?: string;
|
||||
}
|
||||
|
||||
const defaultOptions: GeoJSONMapperConfig = {
|
||||
src: 'https://openlayers.org/en/latest/examples/data/geojson/countries.geojson',
|
||||
};
|
||||
|
||||
export const geojsonMapper: MapLayerRegistryItem<GeoJSONMapperConfig> = {
|
||||
id: 'geojson-value-mapper',
|
||||
name: 'Map values to GeoJSON file',
|
||||
description: 'color features based on query results',
|
||||
isBaseMap: false,
|
||||
|
||||
/**
|
||||
* Function that configures transformation and returns a transformer
|
||||
* @param options
|
||||
*/
|
||||
create: (map: Map, options: MapLayerConfig<GeoJSONMapperConfig>, theme: GrafanaTheme2): MapLayerHandler => {
|
||||
const config = { ...defaultOptions, ...options.config };
|
||||
|
||||
const source = new VectorSource({
|
||||
url: config.src,
|
||||
format: new GeoJSON(),
|
||||
});
|
||||
|
||||
const vectorLayer = new VectorLayer({
|
||||
source,
|
||||
});
|
||||
|
||||
return {
|
||||
init: () => vectorLayer,
|
||||
update: (data: PanelData) => {
|
||||
console.log( "todo... find values matching the ID and update");
|
||||
|
||||
// Update each feature
|
||||
source.getFeatures().forEach( f => {
|
||||
console.log( "Find: ", f.getId(), f.getProperties() );
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
// fill in the default values
|
||||
defaultOptions,
|
||||
};
|
12
public/app/plugins/panel/geomap/layers/data/index.ts
Normal file
12
public/app/plugins/panel/geomap/layers/data/index.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { geojsonMapper } from './geojsonMapper';
|
||||
import { lastPointTracker } from './lastPointTracker';
|
||||
import { worldmapBehaviorLayer } from './worldmapBehavior';
|
||||
|
||||
/**
|
||||
* Registry for layer handlers
|
||||
*/
|
||||
export const dataLayers = [
|
||||
worldmapBehaviorLayer, // mimic the existing worldmap
|
||||
lastPointTracker,
|
||||
geojsonMapper, // dummy for now
|
||||
];
|
@ -0,0 +1,78 @@
|
||||
import { MapLayerRegistryItem, MapLayerConfig, MapLayerHandler, PanelData, Field, GrafanaTheme2 } from '@grafana/data';
|
||||
import Map from 'ol/Map';
|
||||
import Feature from 'ol/Feature';
|
||||
import * as style from 'ol/style';
|
||||
import * as source from 'ol/source';
|
||||
import * as layer from 'ol/layer';
|
||||
import Point from 'ol/geom/Point';
|
||||
import { fromLonLat } from 'ol/proj';
|
||||
|
||||
export interface LastPointConfig {
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
const defaultOptions: LastPointConfig = {
|
||||
icon: 'https://openlayers.org/en/latest/examples/data/icon.png',
|
||||
};
|
||||
|
||||
export const lastPointTracker: MapLayerRegistryItem<LastPointConfig> = {
|
||||
id: 'last-point-tracker',
|
||||
name: 'Icon at last point',
|
||||
description: 'Show an icon at the last point',
|
||||
isBaseMap: false,
|
||||
|
||||
/**
|
||||
* Function that configures transformation and returns a transformer
|
||||
* @param options
|
||||
*/
|
||||
create: (map: Map, options: MapLayerConfig<LastPointConfig>, theme: GrafanaTheme2): MapLayerHandler => {
|
||||
const point = new Feature({});
|
||||
const config = { ...defaultOptions, ...options.config };
|
||||
|
||||
point.setStyle(
|
||||
new style.Style({
|
||||
image: new style.Icon({
|
||||
src: config.icon,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
const vectorSource = new source.Vector({
|
||||
features: [point],
|
||||
});
|
||||
|
||||
const vectorLayer = new layer.Vector({
|
||||
source: vectorSource,
|
||||
});
|
||||
|
||||
return {
|
||||
init: () => vectorLayer,
|
||||
update: (data: PanelData) => {
|
||||
const frame = data.series[0];
|
||||
if (frame && frame.length) {
|
||||
let lat: Field | undefined = undefined;
|
||||
let lng: Field | undefined = undefined;
|
||||
for (const field of frame.fields) {
|
||||
if (field.name === 'lat') {
|
||||
lat = field;
|
||||
} else if (field.name === 'lng') {
|
||||
lng = field;
|
||||
}
|
||||
}
|
||||
|
||||
if (lat && lng) {
|
||||
const idx = lat.values.length - 1;
|
||||
const latV = lat.values.get(idx);
|
||||
const lngV = lng.values.get(idx);
|
||||
if (latV != null && lngV != null) {
|
||||
point.setGeometry(new Point(fromLonLat([lngV, latV])));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
// fill in the default values
|
||||
defaultOptions,
|
||||
};
|
@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { MapLayerRegistryItem, MapLayerConfig, MapLayerHandler, PanelData, GrafanaTheme2 } from '@grafana/data';
|
||||
import Map from 'ol/Map';
|
||||
import Feature from 'ol/Feature';
|
||||
import * as layer from 'ol/layer';
|
||||
import * as source from 'ol/source';
|
||||
import * as style from 'ol/style';
|
||||
import {Point} from 'ol/geom';
|
||||
import { fromLonLat } from 'ol/proj';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { SimpleLegend } from '../../components/SimpleLegend';
|
||||
|
||||
export interface WorldmapConfig {
|
||||
// anything
|
||||
}
|
||||
|
||||
const defaultOptions: WorldmapConfig = {
|
||||
// icon: 'https://openlayers.org/en/latest/examples/data/icon.png',
|
||||
};
|
||||
|
||||
export const worldmapBehaviorLayer: MapLayerRegistryItem<WorldmapConfig> = {
|
||||
id: 'worldmap-behavior',
|
||||
name: 'Worldmap behavior',
|
||||
description: 'behave the same as worldmap plugin',
|
||||
isBaseMap: false,
|
||||
|
||||
/**
|
||||
* Function that configures transformation and returns a transformer
|
||||
* @param options
|
||||
*/
|
||||
create: (map: Map, options: MapLayerConfig<WorldmapConfig>, theme: GrafanaTheme2): MapLayerHandler => {
|
||||
// const config = { ...defaultOptions, ...options.config };
|
||||
const vectorLayer = new layer.Vector({});
|
||||
let legendInstance = <SimpleLegend txt={ `initalizing...`}/>;
|
||||
let count = 0;
|
||||
return {
|
||||
init: () => vectorLayer,
|
||||
legend: () => {
|
||||
return legendInstance;
|
||||
},
|
||||
update: (data: PanelData) => {
|
||||
count++;
|
||||
const features:Feature[] = [];
|
||||
for( let x=0; x<100; x+=20) {
|
||||
for( let y=0; y<40; y+=10) {
|
||||
const dot = new Feature({
|
||||
geometry: new Point(fromLonLat([x,y])),
|
||||
});
|
||||
dot.setStyle(new style.Style({
|
||||
image: new style.Circle({
|
||||
fill: new style.Fill({
|
||||
color: tinycolor({r:(x*2), g:(y*3), b:0}).toString(),
|
||||
}),
|
||||
radius: (4 + (y*0.5) + (x*0.1)),
|
||||
})
|
||||
}));
|
||||
features.push(dot);
|
||||
}
|
||||
}
|
||||
legendInstance = <SimpleLegend txt={ `Update: ${count}`} data={data}/>;
|
||||
|
||||
const vectorSource = new source.Vector({ features });
|
||||
vectorLayer.setSource(vectorSource);
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
// fill in the default values
|
||||
defaultOptions,
|
||||
};
|
12
public/app/plugins/panel/geomap/layers/registry.ts
Normal file
12
public/app/plugins/panel/geomap/layers/registry.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { MapLayerRegistryItem, Registry } from '@grafana/data';
|
||||
|
||||
import { basemapLayers } from './basemaps';
|
||||
import { dataLayers } from './data';
|
||||
|
||||
/**
|
||||
* Registry for layer handlers
|
||||
*/
|
||||
export const geomapLayerRegistry = new Registry<MapLayerRegistryItem<any>>(() => [
|
||||
...basemapLayers, // simple basemaps
|
||||
...dataLayers, // Layers with update functions
|
||||
]);
|
111
public/app/plugins/panel/geomap/migrations.test.ts
Normal file
111
public/app/plugins/panel/geomap/migrations.test.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { PanelModel, FieldConfigSource } from '@grafana/data';
|
||||
import { mapPanelChangedHandler } from './migrations';
|
||||
|
||||
describe('Worldmap Migrations', () => {
|
||||
let prevFieldConfig: FieldConfigSource;
|
||||
|
||||
beforeEach(() => {
|
||||
prevFieldConfig = {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
};
|
||||
});
|
||||
|
||||
it('simple worldmap', () => {
|
||||
const old: any = {
|
||||
angular: simpleWorldmapConfig,
|
||||
};
|
||||
const panel = {} as PanelModel;
|
||||
panel.options = mapPanelChangedHandler(panel, 'grafana-worldmap-panel', old, prevFieldConfig);
|
||||
expect(panel).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"fieldConfig": Object {
|
||||
"defaults": Object {
|
||||
"decimals": 3,
|
||||
"thresholds": Object {
|
||||
"mode": "absolute",
|
||||
"steps": Array [
|
||||
Object {
|
||||
"color": "#37872D",
|
||||
"value": -Infinity,
|
||||
},
|
||||
Object {
|
||||
"color": "#E0B400",
|
||||
"value": 0,
|
||||
},
|
||||
Object {
|
||||
"color": "#C4162A",
|
||||
"value": 50,
|
||||
},
|
||||
Object {
|
||||
"color": "#8F3BB8",
|
||||
"value": 100,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"overrides": Array [],
|
||||
},
|
||||
"options": Object {
|
||||
"basemap": Object {
|
||||
"type": "default",
|
||||
},
|
||||
"controls": Object {
|
||||
"mouseWheelZoom": true,
|
||||
"showLegend": true,
|
||||
"showZoom": true,
|
||||
},
|
||||
"layers": Array [],
|
||||
"view": Object {
|
||||
"center": Object {
|
||||
"id": "europe",
|
||||
"lat": 46,
|
||||
"lon": 14,
|
||||
},
|
||||
"zoom": 6,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
const simpleWorldmapConfig = {
|
||||
id: 23763571993,
|
||||
gridPos: {
|
||||
h: 8,
|
||||
w: 12,
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
type: 'grafana-worldmap-panel',
|
||||
title: 'Panel Title',
|
||||
thresholds: '0,50,100',
|
||||
maxDataPoints: 1,
|
||||
circleMaxSize: 30,
|
||||
circleMinSize: 2,
|
||||
colors: ['#37872D', '#E0B400', '#C4162A', '#8F3BB8'],
|
||||
decimals: 3,
|
||||
esMetric: 'Count',
|
||||
hideEmpty: false,
|
||||
hideZero: false,
|
||||
initialZoom: '6',
|
||||
locationData: 'countries',
|
||||
mapCenter: 'Europe',
|
||||
mapCenterLatitude: 46,
|
||||
mapCenterLongitude: 14,
|
||||
mouseWheelZoom: true,
|
||||
showLegend: true,
|
||||
stickyLabels: false,
|
||||
tableQueryOptions: {
|
||||
geohashField: 'geohash',
|
||||
latitudeField: 'latitude',
|
||||
longitudeField: 'longitude',
|
||||
metricField: 'metric',
|
||||
queryType: 'geohash',
|
||||
},
|
||||
unitPlural: '',
|
||||
unitSingle: '',
|
||||
valueName: 'total',
|
||||
datasource: null,
|
||||
};
|
102
public/app/plugins/panel/geomap/migrations.ts
Normal file
102
public/app/plugins/panel/geomap/migrations.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { FieldConfigSource, PanelTypeChangedHandler, Threshold, ThresholdsMode } from '@grafana/data';
|
||||
import { GeomapPanelOptions } from './types';
|
||||
import { MapCenterID } from './view';
|
||||
|
||||
/**
|
||||
* This is called when the panel changes from another panel
|
||||
*/
|
||||
export const mapPanelChangedHandler: PanelTypeChangedHandler = (panel, prevPluginId, prevOptions, prevFieldConfig) => {
|
||||
// Changing from angular/worldmap panel to react/openlayers
|
||||
if (prevPluginId === 'grafana-worldmap-panel' && prevOptions.angular) {
|
||||
const { fieldConfig, options } = worldmapToGeomapOptions({
|
||||
...prevOptions.angular,
|
||||
fieldConfig: prevFieldConfig,
|
||||
});
|
||||
panel.fieldConfig = fieldConfig; // Mutates the incoming panel
|
||||
return options;
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
export function worldmapToGeomapOptions(angular: any): { fieldConfig: FieldConfigSource; options: GeomapPanelOptions } {
|
||||
const fieldConfig: FieldConfigSource = {
|
||||
defaults: {},
|
||||
overrides: [],
|
||||
};
|
||||
|
||||
const options: GeomapPanelOptions = {
|
||||
view: {
|
||||
center: {
|
||||
id: MapCenterID.Zero,
|
||||
},
|
||||
},
|
||||
controls: {
|
||||
showZoom: true,
|
||||
showLegend: Boolean(angular.showLegend),
|
||||
mouseWheelZoom: Boolean(angular.mouseWheelZoom),
|
||||
},
|
||||
basemap: {
|
||||
type: 'default', // was carto
|
||||
},
|
||||
layers: [
|
||||
// TODO? depends on current configs
|
||||
],
|
||||
};
|
||||
|
||||
let v = asNumber(angular.decimals);
|
||||
if (v) {
|
||||
fieldConfig.defaults.decimals = v;
|
||||
}
|
||||
|
||||
// Convert thresholds and color values
|
||||
if (angular.thresholds && angular.colors) {
|
||||
const levels = angular.thresholds.split(',').map((strVale: string) => {
|
||||
return Number(strVale.trim());
|
||||
});
|
||||
|
||||
// One more color than threshold
|
||||
const thresholds: Threshold[] = [];
|
||||
for (const color of angular.colors) {
|
||||
const idx = thresholds.length - 1;
|
||||
if (idx >= 0) {
|
||||
thresholds.push({ value: levels[idx], color });
|
||||
} else {
|
||||
thresholds.push({ value: -Infinity, color });
|
||||
}
|
||||
}
|
||||
|
||||
fieldConfig.defaults.thresholds = {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
steps: thresholds,
|
||||
};
|
||||
}
|
||||
|
||||
v = asNumber(angular.initialZoom);
|
||||
if (v) {
|
||||
options.view.zoom = v;
|
||||
}
|
||||
|
||||
// mapCenter: 'Europe',
|
||||
// mapCenterLatitude: 46,
|
||||
// mapCenterLongitude: 14,
|
||||
//
|
||||
// Map center (from worldmap)
|
||||
const mapCenters: any = {
|
||||
'(0°, 0°)': MapCenterID.Zero,
|
||||
'North America': 'north-america',
|
||||
Europe: 'europe',
|
||||
'West Asia': 'west-asia',
|
||||
'SE Asia': 'se-asia',
|
||||
'Last GeoHash': MapCenterID.LastPoint,
|
||||
};
|
||||
options.view.center.id = mapCenters[angular.mapCenter as any];
|
||||
options.view.center.lat = asNumber(angular.mapCenterLatitude);
|
||||
options.view.center.lon = asNumber(angular.mapCenterLongitude);
|
||||
return { fieldConfig, options };
|
||||
}
|
||||
|
||||
function asNumber(v: any): number | undefined {
|
||||
const num = +v;
|
||||
return isNaN(num) ? undefined : num;
|
||||
}
|
103
public/app/plugins/panel/geomap/module.tsx
Normal file
103
public/app/plugins/panel/geomap/module.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import { PanelPlugin } from '@grafana/data';
|
||||
import { BaseLayerEditor } from './editor/BaseLayerEditor';
|
||||
import { DataLayersEditor } from './editor/DataLayersEditor';
|
||||
import { GeomapPanel } from './GeomapPanel';
|
||||
import { MapCenterEditor } from './editor/MapCenterEditor';
|
||||
import { defaultView, GeomapPanelOptions } from './types';
|
||||
import { MapZoomEditor } from './editor/MapZoomEditor';
|
||||
import { mapPanelChangedHandler } from './migrations';
|
||||
|
||||
export const plugin = new PanelPlugin<GeomapPanelOptions>(GeomapPanel)
|
||||
.setNoPadding()
|
||||
.setPanelChangeHandler(mapPanelChangedHandler)
|
||||
.useFieldConfig()
|
||||
.setPanelOptions((builder) => {
|
||||
let category = ['Map View'];
|
||||
builder.addCustomEditor({
|
||||
category,
|
||||
id: 'view.center',
|
||||
path: 'view.center',
|
||||
name: 'Center',
|
||||
editor: MapCenterEditor,
|
||||
defaultValue: defaultView.center,
|
||||
});
|
||||
|
||||
builder.addCustomEditor({
|
||||
category,
|
||||
id: 'view.zoom',
|
||||
path: 'view.zoom',
|
||||
name: 'Initial zoom',
|
||||
editor: MapZoomEditor,
|
||||
defaultValue: defaultView.zoom,
|
||||
});
|
||||
|
||||
builder.addBooleanSwitch({
|
||||
category,
|
||||
path: 'view.shared',
|
||||
description: 'Use the same view across multiple panels. Note: this may require a dashboard reload.',
|
||||
name: 'Share view',
|
||||
defaultValue: defaultView.shared,
|
||||
});
|
||||
|
||||
// Nested
|
||||
builder.addCustomEditor({
|
||||
category: ['Base Layer'],
|
||||
id: 'basemap',
|
||||
path: 'basemap',
|
||||
name: 'Base Layer',
|
||||
editor: BaseLayerEditor,
|
||||
});
|
||||
|
||||
builder.addCustomEditor({
|
||||
category: ['Data Layer'],
|
||||
id: 'layers',
|
||||
path: 'layers',
|
||||
name: 'Data Layer',
|
||||
editor: DataLayersEditor,
|
||||
});
|
||||
|
||||
// The controls section
|
||||
category = ['Map Controls'];
|
||||
builder
|
||||
.addBooleanSwitch({
|
||||
category,
|
||||
path: 'controls.showZoom',
|
||||
description: 'show buttons in the upper left',
|
||||
name: 'Show zoom control',
|
||||
defaultValue: true,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
category,
|
||||
path: 'controls.mouseWheelZoom',
|
||||
name: 'Mouse wheel zoom',
|
||||
defaultValue: true,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
category,
|
||||
path: 'controls.showLegend',
|
||||
name: 'Show legend',
|
||||
description: 'Show legend',
|
||||
defaultValue: true,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
category,
|
||||
path: 'controls.showAttribution',
|
||||
name: 'Show attribution',
|
||||
description: 'Show the map source attribution info in the lower right',
|
||||
defaultValue: true,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
category,
|
||||
path: 'controls.showScale',
|
||||
name: 'Show scale',
|
||||
description: 'Indicate map scale',
|
||||
defaultValue: false,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
category,
|
||||
path: 'controls.showDebug',
|
||||
name: 'Show debug',
|
||||
description: 'show map info',
|
||||
defaultValue: false,
|
||||
});
|
||||
});
|
18
public/app/plugins/panel/geomap/plugin.json
Normal file
18
public/app/plugins/panel/geomap/plugin.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "panel",
|
||||
"name": "Geomap",
|
||||
"id": "geomap",
|
||||
"state": "alpha",
|
||||
|
||||
"info": {
|
||||
"description": "Geomap panel",
|
||||
"author": {
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://grafana.com"
|
||||
},
|
||||
"logos": {
|
||||
"small": "img/icn-geomap.svg",
|
||||
"large": "img/icn-geomap.svg"
|
||||
}
|
||||
}
|
||||
}
|
52
public/app/plugins/panel/geomap/types.ts
Normal file
52
public/app/plugins/panel/geomap/types.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { MapLayerConfig } from '@grafana/data';
|
||||
import Units from 'ol/proj/Units';
|
||||
import { MapCenterID } from './view';
|
||||
|
||||
export interface ControlsOptions {
|
||||
// Zoom (upper left)
|
||||
showZoom?: boolean;
|
||||
|
||||
// let the mouse wheel zoom
|
||||
mouseWheelZoom?: boolean;
|
||||
|
||||
// Add legend control
|
||||
showLegend?: boolean;
|
||||
|
||||
// Lower right
|
||||
showAttribution?: boolean;
|
||||
|
||||
// Scale options
|
||||
showScale?: boolean;
|
||||
scaleUnits?: Units;
|
||||
|
||||
// Show debug
|
||||
showDebug?: boolean;
|
||||
}
|
||||
|
||||
export interface MapCenterConfig {
|
||||
id: string; // placename > lookup
|
||||
lat?: number;
|
||||
lon?: number;
|
||||
}
|
||||
|
||||
export interface MapViewConfig {
|
||||
center: MapCenterConfig;
|
||||
zoom?: number;
|
||||
minZoom?: number;
|
||||
maxZoom?: number;
|
||||
shared?: boolean;
|
||||
}
|
||||
|
||||
export const defaultView: MapViewConfig = {
|
||||
center: {
|
||||
id: MapCenterID.Zero,
|
||||
},
|
||||
zoom: 1,
|
||||
};
|
||||
|
||||
export interface GeomapPanelOptions {
|
||||
view: MapViewConfig;
|
||||
controls: ControlsOptions;
|
||||
basemap: MapLayerConfig;
|
||||
layers: MapLayerConfig[];
|
||||
}
|
53
public/app/plugins/panel/geomap/view.ts
Normal file
53
public/app/plugins/panel/geomap/view.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { Registry, RegistryItem } from '@grafana/data';
|
||||
|
||||
interface MapCenterItems extends RegistryItem {
|
||||
lat?: number;
|
||||
lon?: number;
|
||||
}
|
||||
|
||||
export enum MapCenterID {
|
||||
Zero = 'zero',
|
||||
Coordinates = 'coords',
|
||||
LastPoint = 'last',
|
||||
}
|
||||
|
||||
export const centerPointRegistry = new Registry<MapCenterItems>(() => [
|
||||
{
|
||||
id: MapCenterID.Zero as string,
|
||||
name: '(0°, 0°)',
|
||||
lat: 0,
|
||||
lon: 0,
|
||||
},
|
||||
{
|
||||
id: 'north-america',
|
||||
name: 'North America',
|
||||
lat: 40,
|
||||
lon: -100,
|
||||
},
|
||||
{
|
||||
id: 'europe',
|
||||
name: 'Europe',
|
||||
lat: 46,
|
||||
lon: 14,
|
||||
},
|
||||
{
|
||||
id: 'west-asia',
|
||||
name: 'West Asia',
|
||||
lat: 26,
|
||||
lon: 53,
|
||||
},
|
||||
{
|
||||
id: 'se-asia',
|
||||
name: 'South-east Asia',
|
||||
lat: 10,
|
||||
lon: 106,
|
||||
},
|
||||
{
|
||||
id: MapCenterID.Coordinates as string,
|
||||
name: 'Coordinates',
|
||||
},
|
||||
{
|
||||
id: MapCenterID.LastPoint as string,
|
||||
name: 'Last value',
|
||||
},
|
||||
]);
|
158
yarn.lock
158
yarn.lock
@ -3336,6 +3336,35 @@
|
||||
npmlog "^4.1.2"
|
||||
write-file-atomic "^2.3.0"
|
||||
|
||||
"@mapbox/jsonlint-lines-primitives@~2.0.2":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz#ce56e539f83552b58d10d672ea4d6fc9adc7b234"
|
||||
integrity sha1-zlblOfg1UrWNENZy6k1vya3HsjQ=
|
||||
|
||||
"@mapbox/mapbox-gl-style-spec@^13.14.0":
|
||||
version "13.20.1"
|
||||
resolved "https://registry.yarnpkg.com/@mapbox/mapbox-gl-style-spec/-/mapbox-gl-style-spec-13.20.1.tgz#bcf7c42836025a831a76a1e1348ea549c36daf55"
|
||||
integrity sha512-xVCJ3IbKoPwcPrxxtGAxUqHEVxXi1hnJtLIFqgkuZfnzj0KeRbk3dZlDr/KNo1/doJjIoFgPFUO/HMOT+wXGPA==
|
||||
dependencies:
|
||||
"@mapbox/jsonlint-lines-primitives" "~2.0.2"
|
||||
"@mapbox/point-geometry" "^0.1.0"
|
||||
"@mapbox/unitbezier" "^0.0.0"
|
||||
csscolorparser "~1.0.2"
|
||||
json-stringify-pretty-compact "^2.0.0"
|
||||
minimist "^1.2.5"
|
||||
rw "^1.3.3"
|
||||
sort-object "^0.3.2"
|
||||
|
||||
"@mapbox/point-geometry@^0.1.0":
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz#8a83f9335c7860effa2eeeca254332aa0aeed8f2"
|
||||
integrity sha1-ioP5M1x4YO/6Lu7KJUMyqgru2PI=
|
||||
|
||||
"@mapbox/unitbezier@^0.0.0":
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz#15651bd553a67b8581fb398810c98ad86a34524e"
|
||||
integrity sha1-FWUb1VOme4WB+zmIEMmK2Go0Uk4=
|
||||
|
||||
"@mdx-js/loader@^1.6.22":
|
||||
version "1.6.22"
|
||||
resolved "https://registry.yarnpkg.com/@mdx-js/loader/-/loader-1.6.22.tgz#d9e8fe7f8185ff13c9c8639c048b123e30d322c4"
|
||||
@ -4702,6 +4731,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
|
||||
integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
|
||||
|
||||
"@types/arcgis-rest-api@*":
|
||||
version "10.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/arcgis-rest-api/-/arcgis-rest-api-10.4.4.tgz#c84b26ac7f01deb2829bff10ebbe544c2310fbcd"
|
||||
integrity sha512-5NwSfj4po+03fauyr4F5AxYzu8pbbqmxay+pNr5ef2V3Mj+7OylvV48VKuVoO9m799jhZdH3EQgQBHm3Y6q1Sw==
|
||||
|
||||
"@types/argparse@1.0.38":
|
||||
version "1.0.38"
|
||||
resolved "https://registry.yarnpkg.com/@types/argparse/-/argparse-1.0.38.tgz#a81fd8606d481f873a3800c6ebae4f1d768a56a9"
|
||||
@ -5142,7 +5176,7 @@
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/geojson@*":
|
||||
"@types/geojson@*", "@types/geojson@^7946.0.7":
|
||||
version "7946.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad"
|
||||
integrity sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==
|
||||
@ -5443,6 +5477,16 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/npmlog/-/npmlog-4.1.2.tgz#d070fe6a6b78755d1092a3dc492d34c3d8f871c4"
|
||||
integrity sha512-4QQmOF5KlwfxJ5IGXFIudkeLCdMABz03RcUXu+LCb24zmln8QW6aDjuGl4d4XPVLf2j+FnjelHTP7dvceAFbhA==
|
||||
|
||||
"@types/ol@^6.5.1":
|
||||
version "6.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/ol/-/ol-6.5.1.tgz#148def5e5370da4fdf0da65b0e8657c816d79ed8"
|
||||
integrity sha512-SHQjTKZ0s5EvhSnI0nyXEhmS/Ez+045c0TvSad0bZmwgldOsVTCG4b43G7q9dHjN+FQIET1Y4s/i2JFDsEZqEA==
|
||||
dependencies:
|
||||
"@types/arcgis-rest-api" "*"
|
||||
"@types/geojson" "*"
|
||||
"@types/rbush" "*"
|
||||
"@types/topojson-specification" "*"
|
||||
|
||||
"@types/overlayscrollbars@^1.12.0":
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/overlayscrollbars/-/overlayscrollbars-1.12.0.tgz#98456caceca8ad73bd5bb572632a585074e70764"
|
||||
@ -5524,6 +5568,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c"
|
||||
integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==
|
||||
|
||||
"@types/rbush@*":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/rbush/-/rbush-3.0.0.tgz#b6887d99b159e87ae23cd14eceff34f139842aa6"
|
||||
integrity sha512-W3ue/GYWXBOpkRm0VSoifrP3HV0Ni47aVJWvXyWMcbtpBy/l/K/smBRiJ+fI8f7shXRjZBiux+iJzYbh7VmcZg==
|
||||
|
||||
"@types/reach__router@^1.3.7":
|
||||
version "1.3.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/reach__router/-/reach__router-1.3.7.tgz#de8ab374259ae7f7499fc1373b9697a5f3cd6428"
|
||||
@ -5870,6 +5919,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.1.0.tgz#19cf73a7bcf641965485119726397a096f0049bd"
|
||||
integrity sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA==
|
||||
|
||||
"@types/topojson-specification@*":
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/topojson-specification/-/topojson-specification-1.0.1.tgz#a80cb294290b79f2d674d3f5938c544ed2bd9d80"
|
||||
integrity sha512-ZZYZUgkmUls9Uhxx2WZNt9f/h2+H3abUUjOVmq+AaaDFckC5oAwd+MDp95kBirk+XCXrYj0hfpI6DSUiJMrpYQ==
|
||||
dependencies:
|
||||
"@types/geojson" "*"
|
||||
|
||||
"@types/uglify-js@*":
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.0.4.tgz#96beae23df6f561862a830b4288a49e86baac082"
|
||||
@ -9319,6 +9375,11 @@ css@^3.0.0:
|
||||
source-map "^0.6.1"
|
||||
source-map-resolve "^0.6.0"
|
||||
|
||||
csscolorparser@~1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b"
|
||||
integrity sha1-s085HupNqPPpgjHizNjfnAQfFxs=
|
||||
|
||||
cssdb@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-4.4.0.tgz#3bf2f2a68c10f5c6a08abd92378331ee803cddb0"
|
||||
@ -13311,6 +13372,11 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
|
||||
dependencies:
|
||||
postcss "^7.0.14"
|
||||
|
||||
ieee754@^1.1.12:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
|
||||
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
|
||||
|
||||
ieee754@^1.1.4:
|
||||
version "1.1.13"
|
||||
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84"
|
||||
@ -15034,6 +15100,11 @@ json-stable-stringify@^1.0.0, json-stable-stringify@^1.0.1:
|
||||
dependencies:
|
||||
jsonify "~0.0.0"
|
||||
|
||||
json-stringify-pretty-compact@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/json-stringify-pretty-compact/-/json-stringify-pretty-compact-2.0.0.tgz#e77c419f52ff00c45a31f07f4c820c2433143885"
|
||||
integrity sha512-WRitRfs6BGq4q8gTgOy4ek7iPFXjbra0H3PmDLKm2xnZ+Gh1HUhiKGgCZkSPNULlP7mvfu6FV/mOLhCarspADQ==
|
||||
|
||||
json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
|
||||
@ -15864,6 +15935,11 @@ map-visit@^1.0.0:
|
||||
dependencies:
|
||||
object-visit "^1.0.0"
|
||||
|
||||
mapbox-to-css-font@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/mapbox-to-css-font/-/mapbox-to-css-font-2.4.0.tgz#a23b51664a1ee839beaefade013f2655bee9a390"
|
||||
integrity sha512-v674D0WtpxCXlA6E+sBlG1QJWdUkz/s9qAD91bJSXBGuBL5lL4tJXpoJEftecphCh2SVQCjWMS2vhylc3AIQTg==
|
||||
|
||||
markdown-escapes@^1.0.0:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/markdown-escapes/-/markdown-escapes-1.0.4.tgz#c95415ef451499d7602b91095f3c8e8975f78535"
|
||||
@ -17129,6 +17205,29 @@ octokit-pagination-methods@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4"
|
||||
integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==
|
||||
|
||||
ol-layerswitcher@^3.8.3:
|
||||
version "3.8.3"
|
||||
resolved "https://registry.yarnpkg.com/ol-layerswitcher/-/ol-layerswitcher-3.8.3.tgz#c27c7a7152849a968941a6b7aae978fda8bdfea3"
|
||||
integrity sha512-UwUhalf/sGXjz3rvr0EjwsaUVlJAhyJCfcIPciKk1QdNbMKq/2ZXNKGafOjwP2eDxiqhkvnhpIrDGD8+gQ19Cg==
|
||||
|
||||
ol-mapbox-style@^6.1.1:
|
||||
version "6.3.2"
|
||||
resolved "https://registry.yarnpkg.com/ol-mapbox-style/-/ol-mapbox-style-6.3.2.tgz#5cd1cbb41ecd697d3488fd928976def108a41d3b"
|
||||
integrity sha512-itWZuwZHilztRM9983WmJ+ounaXIS0PdXF8h5xJd7cJhSv02M27w4RQkhiUw35/VLlUdTT/ei3KYi0w2TGDw2A==
|
||||
dependencies:
|
||||
"@mapbox/mapbox-gl-style-spec" "^13.14.0"
|
||||
mapbox-to-css-font "^2.4.0"
|
||||
webfont-matcher "^1.1.0"
|
||||
|
||||
ol@^6.5.0:
|
||||
version "6.5.0"
|
||||
resolved "https://registry.yarnpkg.com/ol/-/ol-6.5.0.tgz#d9cd59081ac34dc4caf0509c3f667748a8207a21"
|
||||
integrity sha512-a5ebahrjF5yCPFle1rc0aHzKp/9A4LlUnjh+S3I+x4EgcvcddDhpOX3WDOs0Pg9/wEElrikHSGEvbeej2Hh4Ug==
|
||||
dependencies:
|
||||
ol-mapbox-style "^6.1.1"
|
||||
pbf "3.2.1"
|
||||
rbush "^3.0.1"
|
||||
|
||||
on-finished@~2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
||||
@ -17724,6 +17823,14 @@ path-type@^4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
|
||||
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
|
||||
|
||||
pbf@3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/pbf/-/pbf-3.2.1.tgz#b4c1b9e72af966cd82c6531691115cc0409ffe2a"
|
||||
integrity sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==
|
||||
dependencies:
|
||||
ieee754 "^1.1.12"
|
||||
resolve-protobuf-schema "^2.1.0"
|
||||
|
||||
pbkdf2@^3.0.3:
|
||||
version "3.0.17"
|
||||
resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6"
|
||||
@ -18912,6 +19019,11 @@ protobufjs@^6.10.2:
|
||||
"@types/node" "^13.7.0"
|
||||
long "^4.0.0"
|
||||
|
||||
protocol-buffers-schema@^3.3.1:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.5.1.tgz#8388e768d383ac8cbea23e1280dfadb79f4122ad"
|
||||
integrity sha512-YVCvdhxWNDP8/nJDyXLuM+UFsuPk4+1PB7WGPVDzm3HTHbzFLxQYeW2iZpS4mmnXrQJGBzt230t/BbEb7PrQaw==
|
||||
|
||||
protocols@^1.1.0, protocols@^1.4.0:
|
||||
version "1.4.8"
|
||||
resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8"
|
||||
@ -19097,6 +19209,11 @@ quick-lru@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
|
||||
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
|
||||
|
||||
quickselect@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018"
|
||||
integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==
|
||||
|
||||
raf-schd@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.2.tgz#bd44c708188f2e84c810bf55fcea9231bcaed8a0"
|
||||
@ -19170,6 +19287,13 @@ raw-loader@4.0.2, raw-loader@^4.0.2:
|
||||
loader-utils "^2.0.0"
|
||||
schema-utils "^3.0.0"
|
||||
|
||||
rbush@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/rbush/-/rbush-3.0.1.tgz#5fafa8a79b3b9afdfe5008403a720cc1de882ecf"
|
||||
integrity sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==
|
||||
dependencies:
|
||||
quickselect "^2.0.0"
|
||||
|
||||
rc-align@^2.4.0:
|
||||
version "2.4.5"
|
||||
resolved "https://registry.yarnpkg.com/rc-align/-/rc-align-2.4.5.tgz#c941a586f59d1017f23a428f0b468663fb7102ab"
|
||||
@ -20549,6 +20673,13 @@ resolve-pathname@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
|
||||
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
|
||||
|
||||
resolve-protobuf-schema@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz#9ca9a9e69cf192bbdaf1006ec1973948aa4a3758"
|
||||
integrity sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==
|
||||
dependencies:
|
||||
protocol-buffers-schema "^3.3.1"
|
||||
|
||||
resolve-url@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
|
||||
@ -20788,7 +20919,7 @@ run-queue@^1.0.0, run-queue@^1.0.3:
|
||||
dependencies:
|
||||
aproba "^1.1.1"
|
||||
|
||||
rw@1:
|
||||
rw@1, rw@^1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4"
|
||||
integrity sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=
|
||||
@ -21445,6 +21576,16 @@ socks@~2.3.2:
|
||||
ip "1.1.5"
|
||||
smart-buffer "^4.1.0"
|
||||
|
||||
sort-asc@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/sort-asc/-/sort-asc-0.1.0.tgz#ab799df61fc73ea0956c79c4b531ed1e9e7727e9"
|
||||
integrity sha1-q3md9h/HPqCVbHnEtTHtHp53J+k=
|
||||
|
||||
sort-desc@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/sort-desc/-/sort-desc-0.1.1.tgz#198b8c0cdeb095c463341861e3925d4ee359a9ee"
|
||||
integrity sha1-GYuMDN6wlcRjNBhh45JdTuNZqe4=
|
||||
|
||||
sort-keys@^1.0.0:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
|
||||
@ -21459,6 +21600,14 @@ sort-keys@^2.0.0:
|
||||
dependencies:
|
||||
is-plain-obj "^1.0.0"
|
||||
|
||||
sort-object@^0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/sort-object/-/sort-object-0.3.2.tgz#98e0d199ede40e07c61a84403c61d6c3b290f9e2"
|
||||
integrity sha1-mODRme3kDgfGGoRAPGHWw7KQ+eI=
|
||||
dependencies:
|
||||
sort-asc "^0.1.0"
|
||||
sort-desc "^0.1.1"
|
||||
|
||||
source-list-map@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
|
||||
@ -23566,6 +23715,11 @@ web-namespaces@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-1.1.4.tgz#bc98a3de60dadd7faefc403d1076d529f5e030ec"
|
||||
integrity sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==
|
||||
|
||||
webfont-matcher@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/webfont-matcher/-/webfont-matcher-1.1.0.tgz#98ce95097b29e31fbe733053e10e571642d1c6c7"
|
||||
integrity sha1-mM6VCXsp4x++czBT4Q5XFkLRxsc=
|
||||
|
||||
webidl-conversions@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
|
||||
|
Loading…
Reference in New Issue
Block a user